From ac4ad4d5f26d5d2803d95a40736c397c79b9da51 Mon Sep 17 00:00:00 2001 From: Philip Schmidt Date: Mon, 16 Oct 2023 11:28:36 +0200 Subject: [PATCH 01/39] App router / server components support --- .gitignore | 9 +- package.json | 21 +- src/api/revalidateAll.ts | 75 ------ src/client.ts | 3 + src/index.ts | 10 +- src/lib/components/ContentCollection.tsx | 6 +- .../components/ContentCollectionProvider.tsx | 5 +- src/lib/components/ContentComponent.tsx | 2 +- src/lib/components/Editable.tsx | 4 +- src/lib/pages/Frontend.tsx | 2 +- src/lib/pages/Preview.tsx | 6 +- src/server.ts | 20 ++ src/server/components/ContentCollection.tsx | 68 +++++ src/server/components/ContentComponent.tsx | 37 +++ src/server/components/Editable.tsx | 44 ++++ src/server/components/NodeRenderer.tsx | 38 +++ .../components/client/BackendIncludes.tsx | 17 ++ .../client/ContentComponentIncludes.tsx | 24 ++ src/server/utils/context.ts | 5 + src/server/utils/dataLoader.ts | 202 ++++++++++++++ src/server/utils/helper.ts | 37 +++ src/server/utils/hooks.ts | 74 ++++++ src/server/utils/nodeTypes.ts | 11 + src/types/index.ts | 20 ++ src/utils/backendIncludes.ts | 41 +++ src/utils/config.ts | 36 +++ src/utils/{helper.ts => dataLoader.ts} | 113 +------- src/utils/hooks.ts | 50 +++- tsconfig.json | 2 +- yarn.lock | 249 ++++++++++-------- 30 files changed, 891 insertions(+), 340 deletions(-) delete mode 100644 src/api/revalidateAll.ts create mode 100644 src/client.ts create mode 100644 src/server.ts create mode 100644 src/server/components/ContentCollection.tsx create mode 100644 src/server/components/ContentComponent.tsx create mode 100644 src/server/components/Editable.tsx create mode 100644 src/server/components/NodeRenderer.tsx create mode 100644 src/server/components/client/BackendIncludes.tsx create mode 100644 src/server/components/client/ContentComponentIncludes.tsx create mode 100644 src/server/utils/context.ts create mode 100644 src/server/utils/dataLoader.ts create mode 100644 src/server/utils/helper.ts create mode 100644 src/server/utils/hooks.ts create mode 100644 src/server/utils/nodeTypes.ts create mode 100644 src/utils/backendIncludes.ts create mode 100644 src/utils/config.ts rename src/utils/{helper.ts => dataLoader.ts} (66%) diff --git a/.gitignore b/.gitignore index 0ff72ad..f5cd7d9 100644 --- a/.gitignore +++ b/.gitignore @@ -13,12 +13,15 @@ dist-ssr *.local api/ -index.d.ts -index.d.ts.map -index.js lib/ types/ utils/ +server/ +index.* +server.* +client.* + +!/src/**/* # Editor directories and files .vscode/* diff --git a/package.json b/package.json index d4f99a4..e128f68 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,13 @@ "module": "index.js", "types": "index.d.ts", "exports": { - ".": "./index.js" + ".": "./index.js", + "./server": "./server.js", + "./client": "./client.js" }, "scripts": { "build": "yarn clean && yarn tsc --project tsconfig.json && yarn babel --config-file ./config/babel.config.js src --out-dir . --extensions \".tsx,.ts,.js,.jsx\"", - "clean": "rm -rf index.d.ts* index.js api lib types utils", + "clean": "rm -rf index.d.ts index.d.ts.map index.js server.d.ts server.d.ts.map server.js client.d.ts client.d.ts.map client.js api lib types utils server", "dev": "yarn clean && yarn watch", "watch": "yarn tsc --project tsconfig.dev.json", "lint": "eslint src config" @@ -29,9 +31,6 @@ "loglevel": "^1.8.0" }, "devDependencies": { - "@types/node": "^18.7.21", - "@types/react": "^18.0.17", - "@types/react-dom": "^18.0.6", "@babel/cli": "^7.17.10", "@babel/core": "^7.18.2", "@babel/plugin-proposal-optional-catch-binding": "^7.16.7", @@ -39,21 +38,23 @@ "@babel/preset-env": "^7.18.2", "@babel/preset-react": "^7.17.12", "@babel/preset-typescript": "^7.17.12", - "classnames": "^2.3.1", + "@types/node": "^18.7.21", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", "eslint": "^8.23.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-simple-import-sort": "^8.0.0", - "next": "^12.2.0", + "next": "^13.4.0", "prettier": "^2.7.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "typescript": "^4.6.4" + "typescript": "^5.1.0" }, "peerDependencies": { + "next": "^12.2.0 || ^13.0.0", "react": "^18.2.0", - "react-dom": "^18.2.0", - "next": "^12.2.0 || ^13.0.0" + "react-dom": "^18.2.0" }, "files": [ "src", diff --git a/src/api/revalidateAll.ts b/src/api/revalidateAll.ts deleted file mode 100644 index 87fcf1a..0000000 --- a/src/api/revalidateAll.ts +++ /dev/null @@ -1,75 +0,0 @@ -import PromisePool from '@supercharge/promise-pool/dist'; -import log from 'loglevel'; -import type { NextApiRequest, NextApiResponse } from 'next'; -import { ApiErrors, DocumentsResponse } from '../types'; -import { buildNeosHeaders } from '../utils/helper'; - -log.setDefaultLevel(log.levels.DEBUG); - -export default async function NeosRevalidateAll(req: NextApiRequest, res: NextApiResponse) { - // Check for secret to confirm this is a valid request - if (req.headers.authorization !== `Bearer ${process.env.REVALIDATE_TOKEN}`) { - log.warn('Invalid token for revalidate request'); - - return res.status(401).json({ error: 'Invalid token', revalidated: false }); - } - - const startTime = Date.now(); - - try { - const apiUrl = process.env.NEOS_BASE_URL; - if (!apiUrl) { - throw new Error('Missing NEOS_BASE_URL environment variable'); - } - const fetchUrl = apiUrl + '/neos/content-api/documents'; - const response = await fetch(fetchUrl, { - headers: buildNeosHeaders(), - }); - - if (!response.ok) { - const data: ApiErrors = await response.json(); - if (data.errors) { - const flatErrors = data.errors.map((e) => e.message).join(', '); - log.error('revalidate: error fetching from content API with url', fetchUrl, ':', flatErrors); - throw new Error('Content API responded with error: ' + flatErrors); - } - } - - const data: DocumentsResponse = await response.json(); - - await PromisePool.withConcurrency(revalidateConcurrency()) - .for(data.documents) - .process(async ({ routePath }) => { - log.debug('Revalidating', routePath); - await res.revalidate(routePath); - }); - - const endTime = Date.now(); - log.debug('Revalidation done after, took', `${endTime - startTime}ms`); - - res.json({ revalidated: true }); - - return; - } catch (err) { - // If there was an error, Next.js will continue - // to show the last successfully generated page - log.error('Error revalidating', err); - return res.status(500).json({ error: 'Error revalidating', revalidated: false }); - } -} - -const defaultRevalidateConcurrency = 2; - -const revalidateConcurrency = () => { - const concurrency = process.env.REVALIDATE_CONCURRENCY; - if (!concurrency) { - return defaultRevalidateConcurrency; - } - - const concurrencyInt = parseInt(concurrency, 10); - if (isNaN(concurrencyInt)) { - return defaultRevalidateConcurrency; - } - - return concurrencyInt; -}; diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 0000000..b84aed6 --- /dev/null +++ b/src/client.ts @@ -0,0 +1,3 @@ +'use client'; + +export { default as BackendIncludes } from './server/components/client/BackendIncludes'; diff --git a/src/index.ts b/src/index.ts index f788d55..1e2a00c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,19 +1,21 @@ export * from './types'; export { default as NeosRevalidate } from './api/revalidate'; -export { default as NeosRevalidateAll } from './api/revalidateAll'; +export { default as NeosRevalidateAll } from './api/revalidate'; export { default as NeosContext } from './utils/context'; export { - injectNeosBackendMetadata, loadStaticPaths, loadStaticProps, loadServerSideDocumentProps, loadServerSideNodeProps, routePathToSlug, - withZebra, -} from './utils/helper'; +} from './utils/dataLoader'; + +export { injectNeosBackendMetadata } from './utils/backendIncludes'; + +export { withZebra } from './utils/config'; export { useNode, diff --git a/src/lib/components/ContentCollection.tsx b/src/lib/components/ContentCollection.tsx index 611957e..adf7cbe 100644 --- a/src/lib/components/ContentCollection.tsx +++ b/src/lib/components/ContentCollection.tsx @@ -1,7 +1,5 @@ -import classNames from 'classnames'; import { useContext } from 'react'; -import { NeosContentNode } from '../../types'; import NeosContext from '../../utils/context'; import { useContentCollection, useInBackend } from '../../utils/hooks'; import ChildNodes from './ChildNodes'; @@ -27,9 +25,7 @@ export default function ContentCollection({ as = 'div', nodeName, ...rest }: Con return ( diff --git a/src/lib/components/ContentCollectionProvider.tsx b/src/lib/components/ContentCollectionProvider.tsx index a14d3d3..fa4cdb1 100644 --- a/src/lib/components/ContentCollectionProvider.tsx +++ b/src/lib/components/ContentCollectionProvider.tsx @@ -19,7 +19,10 @@ export default function ContentCollectionProvider({ nodeName, children }: Conten return ( - {children({ collectionProps, children: })} + {children({ + collectionProps, + children: , + })} ); } diff --git a/src/lib/components/ContentComponent.tsx b/src/lib/components/ContentComponent.tsx index 2fb5477..f521ddc 100644 --- a/src/lib/components/ContentComponent.tsx +++ b/src/lib/components/ContentComponent.tsx @@ -1,4 +1,4 @@ -import { useContentComponent, useInBackend } from '../../utils/hooks'; +import { useContentComponent } from '../../utils/hooks'; type ContentComponentProps = { as?: keyof JSX.IntrinsicElements; diff --git a/src/lib/components/Editable.tsx b/src/lib/components/Editable.tsx index ce77f95..7ce5efa 100644 --- a/src/lib/components/Editable.tsx +++ b/src/lib/components/Editable.tsx @@ -1,5 +1,3 @@ -import classNames from 'classnames'; - import { useEditPreviewMode, useInBackend, useNode } from '../../utils/hooks'; type EditableProps = { @@ -21,7 +19,7 @@ export default function Editable({ as = 'div', property, ...rest }: EditableProp return ( { + const neosContext = useContext(NeosServerContext); + + const inBackend = neosContext.inBackend ?? false; + + const neosData = inBackend + ? await loadPreviewDocumentPropsCached(neosContext.contextNodePath) + : await loadDocumentPropsCached(neosContext.routePath); + + if (!neosData) { + return
Could not load data
; + } + + const currentNode = resolveCurrentNode(neosContext, neosData); + + const collectionNode = nodeName ? currentNode?.children?.find((child) => child.nodeName === nodeName) : currentNode; + + if (!collectionNode) { + return
Could not find collection node {nodeName}
; + } + + const { className, ...restAttributes } = rest; + const Component = as; + + return ( + + {collectionNode.children?.map((child) => ( + + ))} + {inBackend && ( + + )} + + ); +}; + +const contentCollectionProps = (collectionNode: NeosContentNode, inBackend: boolean) => { + return { + 'data-__neos-node-contextpath': inBackend ? collectionNode.contextPath : undefined, + // Use a fixed fusion path to render an out-of-band preview of this node + 'data-__neos-fusion-path': inBackend ? 'neosNext/previewNode' : undefined, + 'data-__neos-insertion-anchor': inBackend ? true : undefined, + }; +}; + +export default ContentCollection; diff --git a/src/server/components/ContentComponent.tsx b/src/server/components/ContentComponent.tsx new file mode 100644 index 0000000..40b0672 --- /dev/null +++ b/src/server/components/ContentComponent.tsx @@ -0,0 +1,37 @@ +import { useInBackend, useNode } from '../utils/hooks'; +import ContentComponentIncludes from './client/ContentComponentIncludes'; + +type ContentComponentProps = { + as?: keyof JSX.IntrinsicElements; + children: React.ReactNode; + [x: string]: any; +}; + +const ContentComponent = async ({ as = 'div', children, ...rest }: ContentComponentProps) => { + const inBackend = useInBackend(); + const loadNode = useNode(); + const node = await loadNode(); + + if (!node) { + return null; + } + + const Component = as; + + return ( + + {children} + {inBackend && ( + + )} + + ); +}; + +export default ContentComponent; diff --git a/src/server/components/Editable.tsx b/src/server/components/Editable.tsx new file mode 100644 index 0000000..e23bc0e --- /dev/null +++ b/src/server/components/Editable.tsx @@ -0,0 +1,44 @@ +import { useEditPreviewMode, useInBackend, useNode } from '../utils/hooks'; + +type EditableProps = { + as?: keyof JSX.IntrinsicElements; + property: string; + [x: string]: any; +}; + +const Editable = async ({ as = 'div', property, ...rest }: EditableProps) => { + const inBackend = useInBackend(); + const loadNode = useNode(); + const loadEditPreviewMode = useEditPreviewMode(); + + const node = await loadNode(); + const editPreviewMode = await loadEditPreviewMode(); + + if (!node) { + return null; + } + + const { contextPath, nodeType, properties } = node; + const { className, ...restAttributes } = rest; + const Component = as; + + if (!inBackend || editPreviewMode?.isEdit === false) { + return ; + } + + return ( + + ); +}; + +export default Editable; diff --git a/src/server/components/NodeRenderer.tsx b/src/server/components/NodeRenderer.tsx new file mode 100644 index 0000000..6a65d1d --- /dev/null +++ b/src/server/components/NodeRenderer.tsx @@ -0,0 +1,38 @@ +import { useContext } from 'react'; + +import { NeosContentNode } from '../../types'; +import { NeosServerContext } from '../utils/context'; +import { loadDocumentPropsCached, loadPreviewDocumentPropsCached } from '../utils/dataLoader'; +import { getNodeType } from '../utils/nodeTypes'; + +const NodeRenderer = async ({ node }: { node: NeosContentNode }) => { + const neosContext = useContext(NeosServerContext); + + // We just fetch again and hope it will be cached + const neosData = neosContext.inBackend + ? await loadPreviewDocumentPropsCached(neosContext.contextNodePath) + : await loadDocumentPropsCached(neosContext.routePath); + + if (!neosData) { + return
Could not load data
; + } + + const Component = getNodeType(node.nodeType); + + if (!Component) { + return
Could not find mapping for node type {node.nodeType}
; + } + + return ( + + + + ); +}; + +export default NodeRenderer; diff --git a/src/server/components/client/BackendIncludes.tsx b/src/server/components/client/BackendIncludes.tsx new file mode 100644 index 0000000..d23cfcd --- /dev/null +++ b/src/server/components/client/BackendIncludes.tsx @@ -0,0 +1,17 @@ +'use client'; + +import { useEffect } from 'react'; + +import { BackendProps } from '../../../types'; +import { injectNeosBackendMetadata } from '../../../utils/backendIncludes'; + +const BackendIncludes = ({ backend }: { backend?: BackendProps }) => { + // Use useEffect to prevent errors with rehydration to set Neos metadata + useEffect(() => { + injectNeosBackendMetadata(backend); + }, [backend]); + + return null; +}; + +export default BackendIncludes; diff --git a/src/server/components/client/ContentComponentIncludes.tsx b/src/server/components/client/ContentComponentIncludes.tsx new file mode 100644 index 0000000..634cd2f --- /dev/null +++ b/src/server/components/client/ContentComponentIncludes.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { useEffect } from "react"; + +const ContentComponentIncludes = ({ + contextPath, + serializedNode, +}: { + contextPath: string; + serializedNode: any; +}) => { + // Use useEffect to prevent errors with rehydration to set Neos metadata + useEffect(() => { + (window as any)["@Neos.Neos.Ui:Nodes"] = { + ...(window as any)["@Neos.Neos.Ui:Nodes"], + [contextPath]: serializedNode, + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return null; +}; + +export default ContentComponentIncludes; diff --git a/src/server/utils/context.ts b/src/server/utils/context.ts new file mode 100644 index 0000000..97d9f38 --- /dev/null +++ b/src/server/utils/context.ts @@ -0,0 +1,5 @@ +import { createServerContext } from 'react'; + +import { NeosServerContextProps } from '../../types'; + +export const NeosServerContext = createServerContext('neosDataContext', {}); diff --git a/src/server/utils/dataLoader.ts b/src/server/utils/dataLoader.ts new file mode 100644 index 0000000..a58ac7f --- /dev/null +++ b/src/server/utils/dataLoader.ts @@ -0,0 +1,202 @@ +import log from 'loglevel'; +import { headers as nextHeaders } from 'next/headers'; +import { cache } from 'react'; + +import { ApiErrors, NeosData, SiteData } from '../../types'; + +log.setDefaultLevel(log.levels.DEBUG); + +// TODO Add explicit cache configuration for cached / uncached +export const loadDocumentProps = async (params: { slug: string | string[] }) => { + const apiUrl = process.env.NEOS_BASE_URL; + if (!apiUrl) { + throw new Error('Missing NEOS_BASE_URL environment variable'); + } + + const { slug } = params; + + if (typeof slug !== 'string' && !Array.isArray(slug)) { + throw new Error('Missing slug param'); + } + + const path = '/' + (slug && Array.isArray(slug) ? slug.join('/') : slug); + + const startTime = Date.now(); + const fetchUrl = apiUrl + '/neos/content-api/document?path=' + encodeURIComponent(path); + + log.debug('fetching data from content API from URL', fetchUrl); + + const response = await fetch(fetchUrl, { + headers: buildNeosHeaders(), + cache: 'no-store', + }); + + if (!response.ok) { + if (response.status === 404) { + log.debug('content API returned 404 for path', path); + + return undefined; + } + + const data: ApiErrors = await response.json(); + if (data.errors) { + const flatErrors = data.errors.map((e) => e.message).join(', '); + log.error('error fetching from content API with url', fetchUrl, ':', flatErrors); + + return undefined; + } + } + + const data: NeosData = await response.json(); + const endTime = Date.now(); + log.debug('fetched data from content API for path', path, ', took', `${endTime - startTime}ms`); + + return data; +}; + +export const loadPreviewDocumentProps = async (searchParams: { [key: string]: string | string[] | undefined }) => { + const apiUrl = process.env.NEOS_BASE_URL; + if (!apiUrl) { + throw new Error('Missing NEOS_BASE_URL environment variable'); + } + + const contextPath = searchParams['node[__contextNodePath]']; + if (typeof contextPath !== 'string') { + throw new Error('Missing context path query parameter'); + } + + const startTime = Date.now(); + const fetchUrl = apiUrl + '/neos/content-api/document?contextPath=' + encodeURIComponent(contextPath); + const response = await fetch(fetchUrl, { + headers: buildNeosPreviewHeaders(), + cache: 'no-store', + }); + + if (!response.ok) { + if (response.status === 404) { + log.debug('content API returned 404 for context path', contextPath); + + return undefined; + } + + const data: ApiErrors = await response.json(); + if (data.errors) { + const flatErrors = data.errors.map((e) => e.message).join(', '); + log.error('error fetching from content API with url', fetchUrl, ':', flatErrors); + + return undefined; + } + } + + const data: NeosData = await response.json(); + const endTime = Date.now(); + log.debug('fetched data from content API for context path', contextPath, ', took', `${endTime - startTime}ms`); + + return data; +}; + +export const loadDocumentPropsCached = cache((routePath: string | undefined) => { + if (!routePath) { + return undefined; + } + console.debug('Fetching data from Neos inside cache with route path', routePath); + const slug = routePath.split('/').filter((s) => s.length > 0); + return loadDocumentProps({ slug }); +}); + +export const loadPreviewDocumentPropsCached = cache((contextNodePath: string | undefined) => { + if (!contextNodePath) { + return undefined; + } + console.debug('Fetching data from Neos inside cache with context node path', contextNodePath); + const searchParams = { 'node[__contextNodePath]': contextNodePath }; + return loadPreviewDocumentProps(searchParams); +}); + +export const buildNeosHeaders = () => { + const headers: Record = {}; + + // If PUBLIC_BASE_URL is set, we set the X-Forwarded-* headers from it + if (process.env.PUBLIC_BASE_URL) { + applyProxyHeaders(headers, process.env.PUBLIC_BASE_URL); + } + + return headers; +}; + +export const loadSiteProps = async () => { + const apiUrl = process.env.NEOS_BASE_URL; + if (!apiUrl) { + throw new Error('Missing NEOS_BASE_URL environment variable'); + } + + const startTime = Date.now(); + const fetchUrl = apiUrl + '/neos/content-api/site'; + + log.debug('fetching data from content API from URL', fetchUrl); + + const response = await fetch(fetchUrl, { + headers: buildNeosHeaders(), + cache: 'no-store', + }); + + if (!response.ok) { + const data: ApiErrors = await response.json(); + if (data.errors) { + const flatErrors = data.errors.map((e) => e.message).join(', '); + log.error('error fetching from content API with url', fetchUrl, ':', flatErrors); + + return undefined; + } + } + + const data: SiteData = await response.json(); + const endTime = Date.now(); + log.debug('fetched data from content API with url', fetchUrl, ', took', `${endTime - startTime}ms`); + + return data; +}; + +export const buildNeosPreviewHeaders = () => { + const _headers = nextHeaders(); + + const headers: HeadersInit = { + // Pass the cookie to headless API to forward the Neos session + Cookie: _headers.get('cookie') ?? '', + }; + + // If PUBLIC_BASE_URL is set, we set the X-Forwarded-* headers from it + if (process.env.PUBLIC_BASE_URL) { + applyProxyHeaders(headers, process.env.PUBLIC_BASE_URL); + } else { + const _host = _headers.get('host'); + // Set forwarded host and port to make sure URIs in metadata are correct + if (_host) { + // Split host and port from header + const [host, port] = _host.split(':'); + + headers['X-Forwarded-Host'] = host; + if (port) { + headers['X-Forwarded-Port'] = port; + } else { + const _proto = _headers.get('x-forwarded-proto'); + // Check if HTTPS or HTTP request and set default port to make sure Neos does not use port of an internal endpoint + headers['X-Forwarded-Port'] = _proto === 'https' ? '443' : '80'; + headers['X-Forwarded-Proto'] = typeof _proto === 'string' ? _proto : 'http'; + } + } + } + return headers; +}; + +const applyProxyHeaders = (headers: Record, baseUrl: string) => { + const publicBaseUrl = new URL(baseUrl); + headers['X-Forwarded-Host'] = publicBaseUrl.hostname; + if (publicBaseUrl.port) { + headers['X-Forwarded-Port'] = publicBaseUrl.port; + } else { + // Check if HTTPS or HTTP request and set default port to make sure Neos does not use port of an internal endpoint + headers['X-Forwarded-Port'] = publicBaseUrl.protocol === 'https:' ? '443' : '80'; + } + headers['X-Forwarded-Proto'] = publicBaseUrl.protocol === 'https:' ? 'https' : 'http'; +}; diff --git a/src/server/utils/helper.ts b/src/server/utils/helper.ts new file mode 100644 index 0000000..928e7a8 --- /dev/null +++ b/src/server/utils/helper.ts @@ -0,0 +1,37 @@ +import { NeosContentNode, NeosData, NeosServerContextProps } from '../../types'; + +export function resolveCurrentNode( + neosContext: NeosServerContextProps, + neosData: NeosData +): NeosContentNode | undefined { + // Recurse into neosData.node / .children to find a node where identifier == currentNodeIdentifier + // and return that node + + if (!neosContext.currentNodeIdentifier) { + return undefined; + } + + return resolveCurrentNodeRecursive(neosContext.currentNodeIdentifier, neosData.node); +} + +export function resolveCurrentNodeRecursive( + currentNodeIdentifier: string, + node: NeosContentNode +): NeosContentNode | undefined { + if (node.identifier === currentNodeIdentifier) { + return node; + } + + if (!node.children) { + return undefined; + } + + for (const child of node.children) { + const result = resolveCurrentNodeRecursive(currentNodeIdentifier, child); + if (result) { + return result; + } + } + + return undefined; +} diff --git a/src/server/utils/hooks.ts b/src/server/utils/hooks.ts new file mode 100644 index 0000000..a61ece8 --- /dev/null +++ b/src/server/utils/hooks.ts @@ -0,0 +1,74 @@ +import { useContext } from 'react'; + +import { NeosServerContext } from './context'; +import { loadDocumentPropsCached, loadPreviewDocumentPropsCached } from './dataLoader'; +import { resolveCurrentNode } from './helper'; + +export const useMeta = () => { + const neosContext = useContext(NeosServerContext); + + return async () => { + const neosData = neosContext.inBackend + ? await loadPreviewDocumentPropsCached(neosContext.contextNodePath) + : await loadDocumentPropsCached(neosContext.routePath); + + return neosData?.meta; + }; +}; + +export const useNode = () => { + const neosContext = useContext(NeosServerContext); + + return async () => { + const neosData = neosContext.inBackend + ? await loadPreviewDocumentPropsCached(neosContext.contextNodePath) + : await loadDocumentPropsCached(neosContext.routePath); + + if (!neosData) { + return undefined; + } + + const node = resolveCurrentNode(neosContext, neosData); + + return node; + }; +}; + +export const useDocumentNode = () => { + const neosContext = useContext(NeosServerContext); + + return async () => { + const neosData = neosContext.inBackend + ? await loadPreviewDocumentPropsCached(neosContext.contextNodePath) + : await loadDocumentPropsCached(neosContext.routePath); + + return neosData?.node; + }; +}; + +export const useSiteNode = () => { + const neosContext = useContext(NeosServerContext); + + return async () => { + const neosData = neosContext.inBackend + ? await loadPreviewDocumentPropsCached(neosContext.contextNodePath) + : await loadDocumentPropsCached(neosContext.routePath); + + return neosData?.site; + }; +}; + +export const useInBackend = () => { + const neosContext = useContext(NeosServerContext); + return neosContext?.inBackend; +}; + +export const useEditPreviewMode = () => { + const neosContext = useContext(NeosServerContext); + + return async () => { + const neosData = await loadPreviewDocumentPropsCached(neosContext.contextNodePath); + + return neosData?.backend?.editPreviewMode; + }; +}; diff --git a/src/server/utils/nodeTypes.ts b/src/server/utils/nodeTypes.ts new file mode 100644 index 0000000..6a50e1f --- /dev/null +++ b/src/server/utils/nodeTypes.ts @@ -0,0 +1,11 @@ +import { NeosNodeTypes } from '../../types'; + +let nodeTypes: NeosNodeTypes; + +export function initNodeTypes(data: NeosNodeTypes) { + nodeTypes = data; +} + +export function getNodeType(nodeTypeName: string): React.FC | undefined { + return nodeTypes?.[nodeTypeName]; +} diff --git a/src/types/index.ts b/src/types/index.ts index 17918f1..73000c7 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -5,6 +5,17 @@ export interface NeosData { backend?: BackendProps; } +export type SiteData = { + site: { + content: Record; + meta: { + title: string; + nodeType: string; + nodeName: string; + }; + }; +}; + export interface NeosNode { identifier: string; nodeType: string; @@ -53,6 +64,7 @@ export interface BackendProps { documentInformation?: any; editPreviewMode?: BackendEditPreviewMode; guestFrameApplication?: BackendInclude[]; + routePath?: string; } export interface BackendEditPreviewMode { @@ -95,3 +107,11 @@ export interface DocumentsItem { export interface ApiErrors { errors?: { message: string; code: number }[]; } + +export type NeosServerContextProps = { + routePath?: string; + contextNodePath?: string; + inBackend?: boolean; + documentNodeIdentifier?: string; + currentNodeIdentifier?: string; +}; diff --git a/src/utils/backendIncludes.ts b/src/utils/backendIncludes.ts new file mode 100644 index 0000000..5a311bb --- /dev/null +++ b/src/utils/backendIncludes.ts @@ -0,0 +1,41 @@ +import { BackendInclude, BackendProps } from '../types'; + +// Sets expected metadata for the Neos UI and dispatches the Neos.Neos.Ui.ContentReady event +export const injectNeosBackendMetadata = (backend: BackendProps | undefined) => { + (window as any)['@Neos.Neos.Ui:DocumentInformation'] = backend?.documentInformation; + + if (backend?.guestFrameApplication) { + createBackendIncludes(backend.guestFrameApplication); + } + + const event = new CustomEvent('Neos.Neos.Ui.ContentReady'); + window.parent.document.dispatchEvent(event); + + // TODO Check if we can do it differently + document.body.classList.add('neos-backend'); +}; + +// We add the includes explicitly and do not use next/head to have more control over the initialization order. +const createBackendIncludes = (includes: BackendInclude[]) => { + for (let include of includes) { + const elId = `_neos-ui-${include.key}`; + // We perform a very simple check by id to sync the expected and actual presence of the head elements + if (!document.getElementById(elId)) { + const el = document.createElement(include.type); + el.id = elId; + if (el instanceof HTMLLinkElement && include.rel) { + el.rel = include.rel; + } + if (el instanceof HTMLLinkElement && include.href) { + el.href = include.href; + } + if (el instanceof HTMLScriptElement && include.src) { + el.src = include.src; + } + if (include.content) { + el.innerHTML = include.content; + } + document.head.appendChild(el); + } + } +}; diff --git a/src/utils/config.ts b/src/utils/config.ts new file mode 100644 index 0000000..b88b915 --- /dev/null +++ b/src/utils/config.ts @@ -0,0 +1,36 @@ +import { NextConfig } from 'next'; + +export const withZebra = (nextConfig: NextConfig): NextConfig => { + return { + ...nextConfig, + rewrites: async () => { + const baseUrl = process.env.NEOS_BASE_URL ?? ''; + const neosRewrites = [ + { + source: '/neos/:path*', + destination: baseUrl + '/neos/:path*', + }, + { + source: '/media/thumbnail/:path*', + destination: baseUrl + '/media/thumbnail/:path*', + }, + { + source: '/_Resources/:path*', + destination: baseUrl + '/_Resources/:path*', + }, + ]; + + const rewrites = await nextConfig.rewrites?.(); + + if (!rewrites) { + return neosRewrites; + } + if (Array.isArray(rewrites)) { + return rewrites.concat(neosRewrites); + } + + rewrites.afterFiles = rewrites.afterFiles.concat(neosRewrites); + return rewrites; + }, + }; +}; diff --git a/src/utils/helper.ts b/src/utils/dataLoader.ts similarity index 66% rename from src/utils/helper.ts rename to src/utils/dataLoader.ts index bbaee0c..d452e7f 100644 --- a/src/utils/helper.ts +++ b/src/utils/dataLoader.ts @@ -1,9 +1,7 @@ import log from 'loglevel'; -import { GetServerSidePropsContext, GetStaticPathsContext, GetStaticPropsContext, NextConfig } from 'next'; -import { useRouter } from 'next/router'; -import { useEffect } from 'react'; +import { GetServerSidePropsContext, GetStaticPathsContext, GetStaticPropsContext } from 'next'; -import { ApiErrors, BackendInclude, BackendProps, DocumentsResponse, NeosData } from '../types'; +import { ApiErrors, DocumentsResponse, NeosData } from '../types'; log.setDefaultLevel(log.levels.DEBUG); @@ -182,113 +180,6 @@ export const routePathToSlug = (routePath: string): string[] => { return routePath.split('/'); }; -// Sets expected metadata for the Neos UI and dispatches the Neos.Neos.Ui.ContentReady event -export const injectNeosBackendMetadata = (backend: BackendProps | undefined) => { - (window as any)['@Neos.Neos.Ui:DocumentInformation'] = backend?.documentInformation; - - if (backend?.guestFrameApplication) { - createBackendIncludes(backend.guestFrameApplication); - } - - const event = new CustomEvent('Neos.Neos.Ui.ContentReady'); - window.parent.document.dispatchEvent(event); - - // TODO Check if we can do it differently - document.body.classList.add('neos-backend'); -}; - -// We add the includes explicitly and do not use next/head to have more control over the initialization order. -const createBackendIncludes = (includes: BackendInclude[]) => { - for (let include of includes) { - const elId = `_neos-ui-${include.key}`; - // We perform a very simple check by id to sync the expected and actual presence of the head elements - if (!document.getElementById(elId)) { - const el = document.createElement(include.type); - el.id = elId; - if (el instanceof HTMLLinkElement && include.rel) { - el.rel = include.rel; - } - if (el instanceof HTMLLinkElement && include.href) { - el.href = include.href; - } - if (el instanceof HTMLScriptElement && include.src) { - el.src = include.src; - } - if (include.content) { - el.innerHTML = include.content; - } - document.head.appendChild(el); - } - } -}; - -// Hook to notify the iframe host about route changes (with fake unload / load events) -export const useNotifyContentCanvasRouteChanges = () => { - const router = useRouter(); - - const onRouteChangeStart = () => { - // Dispatch an unload event for the ContentCanvas to start the loading animation - const event = new CustomEvent('unload'); - window.dispatchEvent(event); - - // Workaround: we need to reset the initialized state of the document for a correct reset (e.g. focused element) and loading to stop - //@ts-ignore - delete document.__isInitialized; - }; - const onRouteChangeEnd = () => { - // Fire event for iframe host about load and pass reference to iframe as target - const event = new CustomEvent<{ target: { contentWindow: Window } }>('load', { - detail: { target: { contentWindow: window } }, - }); - window.dispatchEvent(event); - }; - useEffect(() => { - router.events.on('routeChangeStart', onRouteChangeStart); - router.events.on('routeChangeComplete', onRouteChangeEnd); - router.events.on('routeChangeError', onRouteChangeEnd); - - return () => { - router.events.off('routeChangeStart', onRouteChangeStart); - router.events.off('routeChangeComplete', onRouteChangeEnd); - router.events.off('routeChangeError', onRouteChangeEnd); - }; - }, [router]); -}; - -export const withZebra = (nextConfig: NextConfig): NextConfig => { - return { - ...nextConfig, - rewrites: async () => { - const neosRewrites = [ - { - source: '/neos/:path*', - destination: process.env.NEOS_BASE_URL + '/neos/:path*', - }, - { - source: '/media/thumbnail/:path*', - destination: process.env.NEOS_BASE_URL + '/media/thumbnail/:path*', - }, - { - source: '/_Resources/:path*', - destination: process.env.NEOS_BASE_URL + '/_Resources/:path*', - }, - ]; - - const rewrites = await nextConfig.rewrites?.(); - - if (!rewrites) { - return neosRewrites; - } - if (Array.isArray(rewrites)) { - return rewrites.concat(neosRewrites); - } - - rewrites.afterFiles = rewrites.afterFiles.concat(neosRewrites); - return rewrites; - }, - }; -}; - export const buildNeosPreviewHeaders = (req: GetServerSidePropsContext['req']) => { const headers: HeadersInit = { // Pass the cookie to content API to forward the Neos session diff --git a/src/utils/hooks.ts b/src/utils/hooks.ts index 1acb1d5..eae4730 100644 --- a/src/utils/hooks.ts +++ b/src/utils/hooks.ts @@ -1,3 +1,4 @@ +import { useRouter } from 'next/router'; import { useContext, useEffect } from 'react'; import { NeosContentNode } from '../types'; @@ -44,6 +45,7 @@ export const useContentComponent = () => { ...(window as any)['@Neos.Neos.Ui:Nodes'], [contextPath]: backend?.serializedNode, }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return { @@ -69,13 +71,6 @@ export const useContentCollection = (nodeName?: string) => { collectionNode = children?.find((childNode) => childNode.nodeName === nodeName); } - if (!collectionNode) { - return { - collectionNode: neosContext.node, - collectionProps: {}, - }; - } - useEffect(() => { if (!collectionNode || !inBackend) return; @@ -83,8 +78,16 @@ export const useContentCollection = (nodeName?: string) => { ...(window as any)['@Neos.Neos.Ui:Nodes'], [collectionNode.contextPath]: collectionNode.backend?.serializedNode, }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [collectionNode]); + if (!collectionNode) { + return { + collectionNode: neosContext.node, + collectionProps: {}, + }; + } + return { collectionNode, collectionProps: { @@ -95,3 +98,36 @@ export const useContentCollection = (nodeName?: string) => { }, }; }; + +// Hook to notify the iframe host about route changes (with fake unload / load events) +export const useNotifyContentCanvasRouteChanges = () => { + const router = useRouter(); + + const onRouteChangeStart = () => { + // Dispatch an unload event for the ContentCanvas to start the loading animation + const event = new CustomEvent('unload'); + window.dispatchEvent(event); + + // Workaround: we need to reset the initialized state of the document for a correct reset (e.g. focused element) and loading to stop + //@ts-ignore + delete document.__isInitialized; + }; + const onRouteChangeEnd = () => { + // Fire event for iframe host about load and pass reference to iframe as target + const event = new CustomEvent<{ target: { contentWindow: Window } }>('load', { + detail: { target: { contentWindow: window } }, + }); + window.dispatchEvent(event); + }; + useEffect(() => { + router.events.on('routeChangeStart', onRouteChangeStart); + router.events.on('routeChangeComplete', onRouteChangeEnd); + router.events.on('routeChangeError', onRouteChangeEnd); + + return () => { + router.events.off('routeChangeStart', onRouteChangeStart); + router.events.off('routeChangeComplete', onRouteChangeEnd); + router.events.off('routeChangeError', onRouteChangeEnd); + }; + }, [router]); +}; diff --git a/tsconfig.json b/tsconfig.json index ede3c0a..10bad2e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,6 @@ "react": ["node_modules/react"] } }, - "include": ["src/**/*"], + "include": ["src/**/*", "client.ts"], "exclude": ["./*.js", "./*.d.ts"] } diff --git a/yarn.lock b/yarn.lock index cede4ff..f9bac3e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1239,75 +1239,55 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@next/env@12.3.4": - version "12.3.4" - resolved "https://registry.yarnpkg.com/@next/env/-/env-12.3.4.tgz#c787837d36fcad75d72ff8df6b57482027d64a47" - integrity sha512-H/69Lc5Q02dq3o+dxxy5O/oNxFsZpdL6WREtOOtOM1B/weonIwDXkekr1KV5DPVPr12IHFPrMrcJQ6bgPMfn7A== - -"@next/swc-android-arm-eabi@12.3.4": - version "12.3.4" - resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.4.tgz#fd1c2dafe92066c6120761c6a39d19e666dc5dd0" - integrity sha512-cM42Cw6V4Bz/2+j/xIzO8nK/Q3Ly+VSlZJTa1vHzsocJRYz8KT6MrreXaci2++SIZCF1rVRCDgAg5PpqRibdIA== - -"@next/swc-android-arm64@12.3.4": - version "12.3.4" - resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.3.4.tgz#11a146dae7b8bca007239b21c616e83f77b19ed4" - integrity sha512-5jf0dTBjL+rabWjGj3eghpLUxCukRhBcEJgwLedewEA/LJk2HyqCvGIwj5rH+iwmq1llCWbOky2dO3pVljrapg== - -"@next/swc-darwin-arm64@12.3.4": - version "12.3.4" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.4.tgz#14ac8357010c95e67327f47082af9c9d75d5be79" - integrity sha512-DqsSTd3FRjQUR6ao0E1e2OlOcrF5br+uegcEGPVonKYJpcr0MJrtYmPxd4v5T6UCJZ+XzydF7eQo5wdGvSZAyA== - -"@next/swc-darwin-x64@12.3.4": - version "12.3.4" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.4.tgz#e7dc63cd2ac26d15fb84d4d2997207fb9ba7da0f" - integrity sha512-PPF7tbWD4k0dJ2EcUSnOsaOJ5rhT3rlEt/3LhZUGiYNL8KvoqczFrETlUx0cUYaXe11dRA3F80Hpt727QIwByQ== - -"@next/swc-freebsd-x64@12.3.4": - version "12.3.4" - resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.4.tgz#fe7ceec58746fdf03f1fcb37ec1331c28e76af93" - integrity sha512-KM9JXRXi/U2PUM928z7l4tnfQ9u8bTco/jb939pdFUHqc28V43Ohd31MmZD1QzEK4aFlMRaIBQOWQZh4D/E5lQ== - -"@next/swc-linux-arm-gnueabihf@12.3.4": - version "12.3.4" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.4.tgz#d7016934d02bfc8bd69818ffb0ae364b77b17af7" - integrity sha512-3zqD3pO+z5CZyxtKDTnOJ2XgFFRUBciOox6EWkoZvJfc9zcidNAQxuwonUeNts6Xbm8Wtm5YGIRC0x+12YH7kw== - -"@next/swc-linux-arm64-gnu@12.3.4": - version "12.3.4" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.4.tgz#43a7bc409b03487bff5beb99479cacdc7bd29af5" - integrity sha512-kiX0vgJGMZVv+oo1QuObaYulXNvdH/IINmvdZnVzMO/jic/B8EEIGlZ8Bgvw8LCjH3zNVPO3mGrdMvnEEPEhKA== - -"@next/swc-linux-arm64-musl@12.3.4": - version "12.3.4" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.4.tgz#4d1db6de6dc982b974cd1c52937111e3e4a34bd3" - integrity sha512-EETZPa1juczrKLWk5okoW2hv7D7WvonU+Cf2CgsSoxgsYbUCZ1voOpL4JZTOb6IbKMDo6ja+SbY0vzXZBUMvkQ== - -"@next/swc-linux-x64-gnu@12.3.4": - version "12.3.4" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.4.tgz#c3b414d77bab08b35f7dd8943d5586f0adb15e38" - integrity sha512-4csPbRbfZbuWOk3ATyWcvVFdD9/Rsdq5YHKvRuEni68OCLkfy4f+4I9OBpyK1SKJ00Cih16NJbHE+k+ljPPpag== - -"@next/swc-linux-x64-musl@12.3.4": - version "12.3.4" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.4.tgz#187a883ec09eb2442a5ebf126826e19037313c61" - integrity sha512-YeBmI+63Ro75SUiL/QXEVXQ19T++58aI/IINOyhpsRL1LKdyfK/35iilraZEFz9bLQrwy1LYAR5lK200A9Gjbg== - -"@next/swc-win32-arm64-msvc@12.3.4": - version "12.3.4" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.4.tgz#89befa84e453ed2ef9a888f375eba565a0fde80b" - integrity sha512-Sd0qFUJv8Tj0PukAYbCCDbmXcMkbIuhnTeHm9m4ZGjCf6kt7E/RMs55Pd3R5ePjOkN7dJEuxYBehawTR/aPDSQ== - -"@next/swc-win32-ia32-msvc@12.3.4": - version "12.3.4" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.4.tgz#cb50c08f0e40ead63642a7f269f0c8254261f17c" - integrity sha512-rt/vv/vg/ZGGkrkKcuJ0LyliRdbskQU+91bje+PgoYmxTZf/tYs6IfbmgudBJk6gH3QnjHWbkphDdRQrseRefQ== - -"@next/swc-win32-x64-msvc@12.3.4": - version "12.3.4" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.4.tgz#d28ea15a72cdcf96201c60a43e9630cd7fda168f" - integrity sha512-DQ20JEfTBZAgF8QCjYfJhv2/279M6onxFjdG/+5B0Cyj00/EdBxiWb2eGGFgQhrBbNv/lsvzFbbi0Ptf8Vw/bg== +"@next/env@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/env/-/env-13.4.19.tgz#46905b4e6f62da825b040343cbc233144e9578d3" + integrity sha512-FsAT5x0jF2kkhNkKkukhsyYOrRqtSxrEhfliniIq0bwWbuXLgyt3Gv0Ml+b91XwjwArmuP7NxCiGd++GGKdNMQ== + +"@next/swc-darwin-arm64@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.19.tgz#77ad462b5ced4efdc26cb5a0053968d2c7dac1b6" + integrity sha512-vv1qrjXeGbuF2mOkhkdxMDtv9np7W4mcBtaDnHU+yJG+bBwa6rYsYSCI/9Xm5+TuF5SbZbrWO6G1NfTh1TMjvQ== + +"@next/swc-darwin-x64@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.19.tgz#aebe38713a4ce536ee5f2a291673e14b715e633a" + integrity sha512-jyzO6wwYhx6F+7gD8ddZfuqO4TtpJdw3wyOduR4fxTUCm3aLw7YmHGYNjS0xRSYGAkLpBkH1E0RcelyId6lNsw== + +"@next/swc-linux-arm64-gnu@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.19.tgz#ec54db65b587939c7b94f9a84800f003a380f5a6" + integrity sha512-vdlnIlaAEh6H+G6HrKZB9c2zJKnpPVKnA6LBwjwT2BTjxI7e0Hx30+FoWCgi50e+YO49p6oPOtesP9mXDRiiUg== + +"@next/swc-linux-arm64-musl@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.19.tgz#1f5e2c1ea6941e7d530d9f185d5d64be04279d86" + integrity sha512-aU0HkH2XPgxqrbNRBFb3si9Ahu/CpaR5RPmN2s9GiM9qJCiBBlZtRTiEca+DC+xRPyCThTtWYgxjWHgU7ZkyvA== + +"@next/swc-linux-x64-gnu@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.19.tgz#96b0882492a2f7ffcce747846d3680730f69f4d1" + integrity sha512-htwOEagMa/CXNykFFeAHHvMJeqZfNQEoQvHfsA4wgg5QqGNqD5soeCer4oGlCol6NGUxknrQO6VEustcv+Md+g== + +"@next/swc-linux-x64-musl@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.19.tgz#f276b618afa321d2f7b17c81fc83f429fb0fd9d8" + integrity sha512-4Gj4vvtbK1JH8ApWTT214b3GwUh9EKKQjY41hH/t+u55Knxi/0wesMzwQRhppK6Ddalhu0TEttbiJ+wRcoEj5Q== + +"@next/swc-win32-arm64-msvc@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.19.tgz#1599ae0d401da5ffca0947823dac577697cce577" + integrity sha512-bUfDevQK4NsIAHXs3/JNgnvEY+LRyneDN788W2NYiRIIzmILjba7LaQTfihuFawZDhRtkYCv3JDC3B4TwnmRJw== + +"@next/swc-win32-ia32-msvc@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.19.tgz#55cdd7da90818f03e4da16d976f0cb22045d16fd" + integrity sha512-Y5kikILFAr81LYIFaw6j/NrOtmiM4Sf3GtOc0pn50ez2GCkr+oejYuKGcwAwq3jiTKuzF6OF4iT2INPoxRycEA== + +"@next/swc-win32-x64-msvc@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.19.tgz#648f79c4e09279212ac90d871646ae12d80cdfce" + integrity sha512-YzA78jBDXMYiINdPdJJwGgPNT3YqBNNGhsthsDoWHL9p24tEJn9ViQf/ZqTbwSpX/RrkPupLfuuTH2sf73JBAw== "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3": version "2.1.8-no-fsevents.3" @@ -1340,10 +1320,10 @@ resolved "https://registry.yarnpkg.com/@supercharge/promise-pool/-/promise-pool-2.3.2.tgz#6366894a7e7bc699bb65e58d8c828113729cf481" integrity sha512-f5+C7zv+QQivcUO1FH5lXi7GcuJ3CFuJF3Eg06iArhUs5ma0szCLEQwIY4+VQyh7m/RLVZdzvr4E4ZDnLe9MNg== -"@swc/helpers@0.4.11": - version "0.4.11" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.11.tgz#db23a376761b3d31c26502122f349a21b592c8de" - integrity sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw== +"@swc/helpers@0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.1.tgz#e9031491aa3f26bfcc974a67f48bd456c8a5357a" + integrity sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg== dependencies: tslib "^2.4.0" @@ -1357,14 +1337,14 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== -"@types/react-dom@^18.0.6": - version "18.0.6" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.6.tgz#36652900024842b74607a17786b6662dd1e103a1" - integrity sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA== +"@types/react-dom@^18.2.0": + version "18.2.7" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.7.tgz#67222a08c0a6ae0a0da33c3532348277c70abb63" + integrity sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA== dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^18.0.17": +"@types/react@*": version "18.0.21" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.21.tgz#b8209e9626bb00a34c76f55482697edd2b43cc67" integrity sha512-7QUCOxvFgnD5Jk8ZKlUAhVcRj7GuJRjnjjiY/IUBWKgOlnvDvTMLD4RTF7NPyVmbRhNrbomZiOepg7M/2Kj1mA== @@ -1373,6 +1353,15 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@^18.2.0": + version "18.2.21" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.21.tgz#774c37fd01b522d0b91aed04811b58e4e0514ed9" + integrity sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/scheduler@*": version "0.16.2" resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" @@ -1504,6 +1493,13 @@ browserslist@^4.21.5: node-releases "^2.0.12" update-browserslist-db "^1.0.11" +busboy@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -1556,10 +1552,10 @@ chokidar@^3.4.0: optionalDependencies: fsevents "~2.3.2" -classnames@^2.3.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" - integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== +client-only@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" + integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== color-convert@^1.9.0: version "1.9.3" @@ -1911,6 +1907,11 @@ glob-parent@^6.0.1: dependencies: is-glob "^4.0.3" +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + glob@^7.1.3, glob@^7.2.0: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -1947,6 +1948,11 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" +graceful-fs@^4.1.2: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + grapheme-splitter@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" @@ -2165,31 +2171,29 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -next@^12.2.0: - version "12.3.4" - resolved "https://registry.yarnpkg.com/next/-/next-12.3.4.tgz#f2780a6ebbf367e071ce67e24bd8a6e05de2fcb1" - integrity sha512-VcyMJUtLZBGzLKo3oMxrEF0stxh8HwuW976pAzlHhI3t8qJ4SROjCrSh1T24bhrbjw55wfZXAbXPGwPt5FLRfQ== +next@^13.4.0: + version "13.4.19" + resolved "https://registry.yarnpkg.com/next/-/next-13.4.19.tgz#2326e02aeedee2c693d4f37b90e4f0ed6882b35f" + integrity sha512-HuPSzzAbJ1T4BD8e0bs6B9C1kWQ6gv8ykZoRWs5AQoiIuqbGHHdQO7Ljuvg05Q0Z24E2ABozHe6FxDvI6HfyAw== dependencies: - "@next/env" "12.3.4" - "@swc/helpers" "0.4.11" + "@next/env" "13.4.19" + "@swc/helpers" "0.5.1" + busboy "1.6.0" caniuse-lite "^1.0.30001406" postcss "8.4.14" - styled-jsx "5.0.7" - use-sync-external-store "1.2.0" + styled-jsx "5.1.1" + watchpack "2.4.0" + zod "3.21.4" optionalDependencies: - "@next/swc-android-arm-eabi" "12.3.4" - "@next/swc-android-arm64" "12.3.4" - "@next/swc-darwin-arm64" "12.3.4" - "@next/swc-darwin-x64" "12.3.4" - "@next/swc-freebsd-x64" "12.3.4" - "@next/swc-linux-arm-gnueabihf" "12.3.4" - "@next/swc-linux-arm64-gnu" "12.3.4" - "@next/swc-linux-arm64-musl" "12.3.4" - "@next/swc-linux-x64-gnu" "12.3.4" - "@next/swc-linux-x64-musl" "12.3.4" - "@next/swc-win32-arm64-msvc" "12.3.4" - "@next/swc-win32-ia32-msvc" "12.3.4" - "@next/swc-win32-x64-msvc" "12.3.4" + "@next/swc-darwin-arm64" "13.4.19" + "@next/swc-darwin-x64" "13.4.19" + "@next/swc-linux-arm64-gnu" "13.4.19" + "@next/swc-linux-arm64-musl" "13.4.19" + "@next/swc-linux-x64-gnu" "13.4.19" + "@next/swc-linux-x64-musl" "13.4.19" + "@next/swc-win32-arm64-msvc" "13.4.19" + "@next/swc-win32-ia32-msvc" "13.4.19" + "@next/swc-win32-x64-msvc" "13.4.19" node-releases@^2.0.12: version "2.0.12" @@ -2474,6 +2478,11 @@ source-map-js@^1.0.2: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -2486,10 +2495,12 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -styled-jsx@5.0.7: - version "5.0.7" - resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.7.tgz#be44afc53771b983769ac654d355ca8d019dff48" - integrity sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA== +styled-jsx@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.1.tgz#839a1c3aaacc4e735fed0781b8619ea5d0009d1f" + integrity sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw== + dependencies: + client-only "0.0.1" supports-color@^5.3.0: version "5.5.0" @@ -2544,10 +2555,10 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -typescript@^4.6.4: - version "4.8.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.3.tgz#d59344522c4bc464a65a730ac695007fdb66dd88" - integrity sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig== +typescript@^5.1.0: + version "5.2.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" + integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" @@ -2595,10 +2606,13 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -use-sync-external-store@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" - integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== +watchpack@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" which@^2.0.1: version "2.0.2" @@ -2626,3 +2640,8 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zod@3.21.4: + version "3.21.4" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" + integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== From b8ea360e00d0cca1c5631f52eacd8bb0a038eff1 Mon Sep 17 00:00:00 2001 From: Philip Schmidt Date: Mon, 16 Oct 2023 12:03:33 +0200 Subject: [PATCH 02/39] fix lint --- .eslintrc.json | 6 +- config/babel.config.js | 2 +- package.json | 2 + src/index.ts | 46 +++--- .../components/ContentCollectionProvider.tsx | 8 +- src/server.ts | 11 +- src/server/components/ContentCollection.tsx | 4 +- .../client/ContentComponentIncludes.tsx | 17 +- src/types/index.ts | 6 + src/utils/backendIncludes.ts | 2 +- src/utils/hooks.ts | 3 - tsconfig.json | 2 +- yarn.lock | 148 +++++++++++++++++- 13 files changed, 198 insertions(+), 59 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 37bceba..6253ab1 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,7 +1,9 @@ { - "plugins": ["simple-import-sort"], - "extends": ["plugin:prettier/recommended"], + "extends": ["plugin:prettier/recommended", "plugin:@typescript-eslint/recommended"], + "parser": "@typescript-eslint/parser", + "plugins": ["simple-import-sort", "@typescript-eslint"], "rules": { + "@typescript-eslint/no-explicit-any": "off", "react/no-unescaped-entities": "off", "simple-import-sort/imports": "warn", "simple-import-sort/exports": "warn", diff --git a/config/babel.config.js b/config/babel.config.js index 3302612..16d036f 100644 --- a/config/babel.config.js +++ b/config/babel.config.js @@ -23,6 +23,6 @@ module.exports = (api) => { ['@babel/preset-typescript', { isTSX: true, allExtensions: true }], ], plugins: ['@babel/plugin-proposal-optional-catch-binding', '@babel/plugin-transform-runtime'], - comments: false + comments: false, }; }; diff --git a/package.json b/package.json index e128f68..8257c9f 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,8 @@ "@types/node": "^18.7.21", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", + "@typescript-eslint/eslint-plugin": "^6.7.5", + "@typescript-eslint/parser": "^6.7.5", "eslint": "^8.23.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", diff --git a/src/index.ts b/src/index.ts index 1e2a00c..33bb2a4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,40 +1,32 @@ -export * from './types'; - export { default as NeosRevalidate } from './api/revalidate'; export { default as NeosRevalidateAll } from './api/revalidate'; - +export { default as BackendContainer } from './lib/components/BackendContainer'; +export { default as ChildNodes } from './lib/components/ChildNodes'; +export { default as ContentCollection } from './lib/components/ContentCollection'; +export { default as ContentCollectionProvider } from './lib/components/ContentCollectionProvider'; +export { default as ContentComponent } from './lib/components/ContentComponent'; +export { default as ContentRegistry } from './lib/components/ContentRegistry'; +export { default as Editable } from './lib/components/Editable'; +export { default as Frontend } from './lib/pages/Frontend'; +export { default as Preview } from './lib/pages/Preview'; +export * from './types'; +export { injectNeosBackendMetadata } from './utils/backendIncludes'; +export { withZebra } from './utils/config'; export { default as NeosContext } from './utils/context'; - export { - loadStaticPaths, - loadStaticProps, loadServerSideDocumentProps, loadServerSideNodeProps, + loadStaticPaths, + loadStaticProps, routePathToSlug, } from './utils/dataLoader'; - -export { injectNeosBackendMetadata } from './utils/backendIncludes'; - -export { withZebra } from './utils/config'; - export { - useNode, - useDocumentNode, - useSiteNode, - useMeta, - useInBackend, useContentCollection, useContentComponent, + useDocumentNode, useEditPreviewMode, + useInBackend, + useMeta, + useNode, + useSiteNode, } from './utils/hooks'; - -export { default as Preview } from './lib/pages/Preview'; -export { default as Frontend } from './lib/pages/Frontend'; - -export { default as ChildNodes } from './lib/components/ChildNodes'; -export { default as ContentCollection } from './lib/components/ContentCollection'; -export { default as ContentCollectionProvider } from './lib/components/ContentCollectionProvider'; -export { default as ContentComponent } from './lib/components/ContentComponent'; -export { default as ContentRegistry } from './lib/components/ContentRegistry'; -export { default as Editable } from './lib/components/Editable'; -export { default as BackendContainer } from './lib/components/BackendContainer'; diff --git a/src/lib/components/ContentCollectionProvider.tsx b/src/lib/components/ContentCollectionProvider.tsx index fa4cdb1..55eef36 100644 --- a/src/lib/components/ContentCollectionProvider.tsx +++ b/src/lib/components/ContentCollectionProvider.tsx @@ -6,7 +6,13 @@ import ChildNodes from './ChildNodes'; type ContentCollectionProviderProps = { nodeName: string; - children: ({ collectionProps, children }: { collectionProps: {}; children: React.ReactNode }) => React.ReactNode; + children: ({ + collectionProps, + children, + }: { + collectionProps: Record; + children: React.ReactNode; + }) => React.ReactNode; }; export default function ContentCollectionProvider({ nodeName, children }: ContentCollectionProviderProps) { diff --git a/src/server.ts b/src/server.ts index bcac46c..faa266a 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,20 +1,15 @@ export { default as BackendContainer } from './lib/components/BackendContainer'; export { default as ContentCollection } from './server/components/ContentCollection'; export { default as ContentComponent } from './server/components/ContentComponent'; -export { default as NodeRenderer } from './server/components/NodeRenderer'; export { default as Editable } from './server/components/Editable'; - +export { default as NodeRenderer } from './server/components/NodeRenderer'; export { NeosServerContext } from './server/utils/context'; - -export { getNodeType, initNodeTypes } from './server/utils/nodeTypes'; - export { loadDocumentProps, loadDocumentPropsCached, loadPreviewDocumentProps, loadPreviewDocumentPropsCached, } from './server/utils/dataLoader'; - export { resolveCurrentNode, resolveCurrentNodeRecursive } from './server/utils/helper'; - -export { useDocumentNode, useMeta, useNode, useSiteNode, useInBackend, useEditPreviewMode } from './server/utils/hooks'; +export { useDocumentNode, useEditPreviewMode, useInBackend, useMeta, useNode, useSiteNode } from './server/utils/hooks'; +export { getNodeType, initNodeTypes } from './server/utils/nodeTypes'; diff --git a/src/server/components/ContentCollection.tsx b/src/server/components/ContentCollection.tsx index dd433c8..7538c9c 100644 --- a/src/server/components/ContentCollection.tsx +++ b/src/server/components/ContentCollection.tsx @@ -1,11 +1,11 @@ import { useContext } from 'react'; +import { NeosContentNode } from '../../types'; import { NeosServerContext } from '../utils/context'; import { loadDocumentPropsCached, loadPreviewDocumentPropsCached } from '../utils/dataLoader'; import { resolveCurrentNode } from '../utils/helper'; -import NodeRenderer from './NodeRenderer'; -import { NeosContentNode } from '../../types'; import ContentComponentIncludes from './client/ContentComponentIncludes'; +import NodeRenderer from './NodeRenderer'; type ContentCollectionProps = { as?: keyof JSX.IntrinsicElements; diff --git a/src/server/components/client/ContentComponentIncludes.tsx b/src/server/components/client/ContentComponentIncludes.tsx index 634cd2f..ba8eecc 100644 --- a/src/server/components/client/ContentComponentIncludes.tsx +++ b/src/server/components/client/ContentComponentIncludes.tsx @@ -1,21 +1,14 @@ -"use client"; +'use client'; -import { useEffect } from "react"; +import { useEffect } from 'react'; -const ContentComponentIncludes = ({ - contextPath, - serializedNode, -}: { - contextPath: string; - serializedNode: any; -}) => { +const ContentComponentIncludes = ({ contextPath, serializedNode }: { contextPath: string; serializedNode: any }) => { // Use useEffect to prevent errors with rehydration to set Neos metadata useEffect(() => { - (window as any)["@Neos.Neos.Ui:Nodes"] = { - ...(window as any)["@Neos.Neos.Ui:Nodes"], + (window as any)['@Neos.Neos.Ui:Nodes'] = { + ...(window as any)['@Neos.Neos.Ui:Nodes'], [contextPath]: serializedNode, }; - // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return null; diff --git a/src/types/index.ts b/src/types/index.ts index 73000c7..291d66a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,9 @@ +declare global { + interface Document { + __isInitialized?: boolean; + } +} + export interface NeosData { node: NeosContentNode; site: NeosNode; diff --git a/src/utils/backendIncludes.ts b/src/utils/backendIncludes.ts index 5a311bb..87bb3cb 100644 --- a/src/utils/backendIncludes.ts +++ b/src/utils/backendIncludes.ts @@ -17,7 +17,7 @@ export const injectNeosBackendMetadata = (backend: BackendProps | undefined) => // We add the includes explicitly and do not use next/head to have more control over the initialization order. const createBackendIncludes = (includes: BackendInclude[]) => { - for (let include of includes) { + for (const include of includes) { const elId = `_neos-ui-${include.key}`; // We perform a very simple check by id to sync the expected and actual presence of the head elements if (!document.getElementById(elId)) { diff --git a/src/utils/hooks.ts b/src/utils/hooks.ts index eae4730..807d055 100644 --- a/src/utils/hooks.ts +++ b/src/utils/hooks.ts @@ -45,7 +45,6 @@ export const useContentComponent = () => { ...(window as any)['@Neos.Neos.Ui:Nodes'], [contextPath]: backend?.serializedNode, }; - // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return { @@ -78,7 +77,6 @@ export const useContentCollection = (nodeName?: string) => { ...(window as any)['@Neos.Neos.Ui:Nodes'], [collectionNode.contextPath]: collectionNode.backend?.serializedNode, }; - // eslint-disable-next-line react-hooks/exhaustive-deps }, [collectionNode]); if (!collectionNode) { @@ -109,7 +107,6 @@ export const useNotifyContentCanvasRouteChanges = () => { window.dispatchEvent(event); // Workaround: we need to reset the initialized state of the document for a correct reset (e.g. focused element) and loading to stop - //@ts-ignore delete document.__isInitialized; }; const onRouteChangeEnd = () => { diff --git a/tsconfig.json b/tsconfig.json index 10bad2e..ede3c0a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,6 @@ "react": ["node_modules/react"] } }, - "include": ["src/**/*", "client.ts"], + "include": ["src/**/*"], "exclude": ["./*.js", "./*.d.ts"] } diff --git a/yarn.lock b/yarn.lock index f9bac3e..56e1aaf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1151,6 +1151,18 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@eslint-community/eslint-utils@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.5.1": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.9.1.tgz#449dfa81a57a1d755b09aa58d826c1262e4283b4" + integrity sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA== + "@eslint/eslintrc@^1.3.2": version "1.3.2" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.2.tgz#58b69582f3b7271d8fa67fe5251767a5b38ea356" @@ -1327,6 +1339,11 @@ dependencies: tslib "^2.4.0" +"@types/json-schema@^7.0.12": + version "7.0.13" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.13.tgz#02c24f4363176d2d18fc8b70b9f3c54aba178a85" + integrity sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ== + "@types/node@^18.7.21": version "18.7.21" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.21.tgz#63ee6688070e456325b6748dc492a7b948593871" @@ -1367,6 +1384,96 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== +"@types/semver@^7.5.0": + version "7.5.3" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.3.tgz#9a726e116beb26c24f1ccd6850201e1246122e04" + integrity sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw== + +"@typescript-eslint/eslint-plugin@^6.7.5": + version "6.7.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.5.tgz#f4024b9f63593d0c2b5bd6e4ca027e6f30934d4f" + integrity sha512-JhtAwTRhOUcP96D0Y6KYnwig/MRQbOoLGXTON2+LlyB/N35SP9j1boai2zzwXb7ypKELXMx3DVk9UTaEq1vHEw== + dependencies: + "@eslint-community/regexpp" "^4.5.1" + "@typescript-eslint/scope-manager" "6.7.5" + "@typescript-eslint/type-utils" "6.7.5" + "@typescript-eslint/utils" "6.7.5" + "@typescript-eslint/visitor-keys" "6.7.5" + debug "^4.3.4" + graphemer "^1.4.0" + ignore "^5.2.4" + natural-compare "^1.4.0" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/parser@^6.7.5": + version "6.7.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.7.5.tgz#8d7ca3d1fbd9d5a58cc4d30b2aa797a760137886" + integrity sha512-bIZVSGx2UME/lmhLcjdVc7ePBwn7CLqKarUBL4me1C5feOd663liTGjMBGVcGr+BhnSLeP4SgwdvNnnkbIdkCw== + dependencies: + "@typescript-eslint/scope-manager" "6.7.5" + "@typescript-eslint/types" "6.7.5" + "@typescript-eslint/typescript-estree" "6.7.5" + "@typescript-eslint/visitor-keys" "6.7.5" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@6.7.5": + version "6.7.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.7.5.tgz#1cf33b991043886cd67f4f3600b8e122fc14e711" + integrity sha512-GAlk3eQIwWOJeb9F7MKQ6Jbah/vx1zETSDw8likab/eFcqkjSD7BI75SDAeC5N2L0MmConMoPvTsmkrg71+B1A== + dependencies: + "@typescript-eslint/types" "6.7.5" + "@typescript-eslint/visitor-keys" "6.7.5" + +"@typescript-eslint/type-utils@6.7.5": + version "6.7.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.7.5.tgz#0a65949ec16588d8956f6d967f7d9c84ddb2d72a" + integrity sha512-Gs0qos5wqxnQrvpYv+pf3XfcRXW6jiAn9zE/K+DlmYf6FcpxeNYN0AIETaPR7rHO4K2UY+D0CIbDP9Ut0U4m1g== + dependencies: + "@typescript-eslint/typescript-estree" "6.7.5" + "@typescript-eslint/utils" "6.7.5" + debug "^4.3.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/types@6.7.5": + version "6.7.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.7.5.tgz#4571320fb9cf669de9a95d9849f922c3af809790" + integrity sha512-WboQBlOXtdj1tDFPyIthpKrUb+kZf2VroLZhxKa/VlwLlLyqv/PwUNgL30BlTVZV1Wu4Asu2mMYPqarSO4L5ZQ== + +"@typescript-eslint/typescript-estree@6.7.5": + version "6.7.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.5.tgz#4578de1a26e9f24950f029a4f00d1bfe41f15a39" + integrity sha512-NhJiJ4KdtwBIxrKl0BqG1Ur+uw7FiOnOThcYx9DpOGJ/Abc9z2xNzLeirCG02Ig3vkvrc2qFLmYSSsaITbKjlg== + dependencies: + "@typescript-eslint/types" "6.7.5" + "@typescript-eslint/visitor-keys" "6.7.5" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/utils@6.7.5": + version "6.7.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.7.5.tgz#ab847b53d6b65e029314b8247c2336843dba81ab" + integrity sha512-pfRRrH20thJbzPPlPc4j0UNGvH1PjPlhlCMq4Yx7EGjV7lvEeGX0U6MJYe8+SyFutWgSHsdbJ3BXzZccYggezA== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "6.7.5" + "@typescript-eslint/types" "6.7.5" + "@typescript-eslint/typescript-estree" "6.7.5" + semver "^7.5.4" + +"@typescript-eslint/visitor-keys@6.7.5": + version "6.7.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.5.tgz#84c68d6ceb5b12d5246b918b84f2b79affd6c2f1" + integrity sha512-3MaWdDZtLlsexZzDSdQWsFQ9l9nL8B80Z4fImSpyllFC/KLqWQRdEcB+gGGO+N3Q2uL40EsG66wZLsohPxNXvg== + dependencies: + "@typescript-eslint/types" "6.7.5" + eslint-visitor-keys "^3.4.1" + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -1624,7 +1731,7 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== -debug@^4.1.0, debug@^4.1.1, debug@^4.3.2: +debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -1717,6 +1824,11 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== +eslint-visitor-keys@^3.4.1: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + eslint@^8.23.0: version "8.24.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.24.0.tgz#489516c927a5da11b3979dbfb2679394523383c8" @@ -1958,6 +2070,11 @@ grapheme-splitter@^1.0.4: resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -1980,6 +2097,11 @@ ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== +ignore@^5.2.4: + version "5.2.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" + integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== + import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -2128,6 +2250,13 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -2451,6 +2580,13 @@ semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.5.4: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -2538,6 +2674,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +ts-api-utils@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331" + integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg== + tslib@^2.4.0: version "2.5.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.2.tgz#1b6f07185c881557b0ffa84b111a0106989e8338" @@ -2636,6 +2777,11 @@ yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" From 10114914de69bcffe0415d38c0a14ff092726693 Mon Sep 17 00:00:00 2001 From: Philip Schmidt Date: Mon, 16 Oct 2023 13:19:17 +0200 Subject: [PATCH 03/39] Add missing export for loadSiteProps --- src/server.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server.ts b/src/server.ts index faa266a..6c422ed 100644 --- a/src/server.ts +++ b/src/server.ts @@ -9,6 +9,7 @@ export { loadDocumentPropsCached, loadPreviewDocumentProps, loadPreviewDocumentPropsCached, + loadSiteProps, } from './server/utils/dataLoader'; export { resolveCurrentNode, resolveCurrentNodeRecursive } from './server/utils/helper'; export { useDocumentNode, useEditPreviewMode, useInBackend, useMeta, useNode, useSiteNode } from './server/utils/hooks'; From e50f32e5b7ea37832b0819751e544463a07618ea Mon Sep 17 00:00:00 2001 From: Philip Schmidt Date: Mon, 16 Oct 2023 13:30:31 +0200 Subject: [PATCH 04/39] Add missing directory in package files --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 8257c9f..1baacf7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@networkteam/zebra", - "version": "0.8.0", + "version": "0.10.0", "author": "networkteam GmbH", "license": "MIT", "publishConfig": { @@ -64,6 +64,7 @@ "*.d.ts*", "api", "lib", + "server", "types", "utils" ] From ec3a7d718837738102c226eac80d488ec0ca3e53 Mon Sep 17 00:00:00 2001 From: Philip Schmidt Date: Mon, 16 Oct 2023 13:32:50 +0200 Subject: [PATCH 05/39] Change package version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1baacf7..956f3f7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@networkteam/zebra", - "version": "0.10.0", + "version": "0.9.0", "author": "networkteam GmbH", "license": "MIT", "publishConfig": { From 2469140f1f1908857d156adcb75256a52de033dd Mon Sep 17 00:00:00 2001 From: Philip Schmidt Date: Mon, 16 Oct 2023 13:40:56 +0200 Subject: [PATCH 06/39] Replace console.debug logs with loglevel --- src/server/utils/dataLoader.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/utils/dataLoader.ts b/src/server/utils/dataLoader.ts index a58ac7f..5dfe2ed 100644 --- a/src/server/utils/dataLoader.ts +++ b/src/server/utils/dataLoader.ts @@ -99,7 +99,7 @@ export const loadDocumentPropsCached = cache((routePath: string | undefined) => if (!routePath) { return undefined; } - console.debug('Fetching data from Neos inside cache with route path', routePath); + log.debug('fetching data from Neos inside cache with route path', routePath); const slug = routePath.split('/').filter((s) => s.length > 0); return loadDocumentProps({ slug }); }); @@ -108,7 +108,7 @@ export const loadPreviewDocumentPropsCached = cache((contextNodePath: string | u if (!contextNodePath) { return undefined; } - console.debug('Fetching data from Neos inside cache with context node path', contextNodePath); + log.debug('fetching data from Neos inside cache with context node path', contextNodePath); const searchParams = { 'node[__contextNodePath]': contextNodePath }; return loadPreviewDocumentProps(searchParams); }); From 88604ae7c93500316d34b4dbf3d26e748417333d Mon Sep 17 00:00:00 2001 From: Philip Schmidt Date: Mon, 16 Oct 2023 13:48:31 +0200 Subject: [PATCH 07/39] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 956f3f7..1baacf7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@networkteam/zebra", - "version": "0.9.0", + "version": "0.10.0", "author": "networkteam GmbH", "license": "MIT", "publishConfig": { From d947d85048266aa21e637ad60f8f653356e54a73 Mon Sep 17 00:00:00 2001 From: Philip Schmidt Date: Thu, 23 Nov 2023 17:37:40 +0100 Subject: [PATCH 08/39] ContentCollection/ContentComponent hooks and providers --- src/server.ts | 13 +++- src/server/components/ContentCollection.tsx | 63 ++++-------------- .../components/ContentCollectionProvider.tsx | 47 ++++++++++++++ src/server/components/ContentComponent.tsx | 29 +++------ .../components/ContentComponentProvider.tsx | 38 +++++++++++ src/server/utils/hooks.ts | 65 ++++++++++++++++++- 6 files changed, 182 insertions(+), 73 deletions(-) create mode 100644 src/server/components/ContentCollectionProvider.tsx create mode 100644 src/server/components/ContentComponentProvider.tsx diff --git a/src/server.ts b/src/server.ts index 6c422ed..cf3c9fc 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,6 +1,8 @@ export { default as BackendContainer } from './lib/components/BackendContainer'; export { default as ContentCollection } from './server/components/ContentCollection'; +export { default as ContentCollectionProvider } from './server/components/ContentCollectionProvider'; export { default as ContentComponent } from './server/components/ContentComponent'; +export { default as ContentComponentProvider } from './server/components/ContentComponentProvider'; export { default as Editable } from './server/components/Editable'; export { default as NodeRenderer } from './server/components/NodeRenderer'; export { NeosServerContext } from './server/utils/context'; @@ -12,5 +14,14 @@ export { loadSiteProps, } from './server/utils/dataLoader'; export { resolveCurrentNode, resolveCurrentNodeRecursive } from './server/utils/helper'; -export { useDocumentNode, useEditPreviewMode, useInBackend, useMeta, useNode, useSiteNode } from './server/utils/hooks'; +export { + useContentCollection, + useContentComponent, + useDocumentNode, + useEditPreviewMode, + useInBackend, + useMeta, + useNode, + useSiteNode, +} from './server/utils/hooks'; export { getNodeType, initNodeTypes } from './server/utils/nodeTypes'; diff --git a/src/server/components/ContentCollection.tsx b/src/server/components/ContentCollection.tsx index 7538c9c..948999c 100644 --- a/src/server/components/ContentCollection.tsx +++ b/src/server/components/ContentCollection.tsx @@ -1,11 +1,5 @@ -import { useContext } from 'react'; - -import { NeosContentNode } from '../../types'; -import { NeosServerContext } from '../utils/context'; -import { loadDocumentPropsCached, loadPreviewDocumentPropsCached } from '../utils/dataLoader'; -import { resolveCurrentNode } from '../utils/helper'; -import ContentComponentIncludes from './client/ContentComponentIncludes'; -import NodeRenderer from './NodeRenderer'; +import { useInBackend } from '../utils/hooks'; +import ContentCollectionProvider from './ContentCollectionProvider'; type ContentCollectionProps = { as?: keyof JSX.IntrinsicElements; @@ -14,55 +8,24 @@ type ContentCollectionProps = { }; const ContentCollection = async ({ as = 'div', nodeName, ...rest }: ContentCollectionProps) => { - const neosContext = useContext(NeosServerContext); - - const inBackend = neosContext.inBackend ?? false; - - const neosData = inBackend - ? await loadPreviewDocumentPropsCached(neosContext.contextNodePath) - : await loadDocumentPropsCached(neosContext.routePath); - - if (!neosData) { - return
Could not load data
; - } - - const currentNode = resolveCurrentNode(neosContext, neosData); - - const collectionNode = nodeName ? currentNode?.children?.find((child) => child.nodeName === nodeName) : currentNode; - - if (!collectionNode) { - return
Could not find collection node {nodeName}
; - } + const inBackend = useInBackend(); const { className, ...restAttributes } = rest; const Component = as; return ( - - {collectionNode.children?.map((child) => ( - - ))} - {inBackend && ( - + + {({ collectionProps, children }) => ( + + {children} + )} - + ); }; -const contentCollectionProps = (collectionNode: NeosContentNode, inBackend: boolean) => { - return { - 'data-__neos-node-contextpath': inBackend ? collectionNode.contextPath : undefined, - // Use a fixed fusion path to render an out-of-band preview of this node - 'data-__neos-fusion-path': inBackend ? 'neosNext/previewNode' : undefined, - 'data-__neos-insertion-anchor': inBackend ? true : undefined, - }; -}; - export default ContentCollection; diff --git a/src/server/components/ContentCollectionProvider.tsx b/src/server/components/ContentCollectionProvider.tsx new file mode 100644 index 0000000..e573444 --- /dev/null +++ b/src/server/components/ContentCollectionProvider.tsx @@ -0,0 +1,47 @@ +import { useInBackend } from '../utils/hooks'; +import { useContentCollection } from '../utils/hooks'; +import ContentComponentIncludes from './client/ContentComponentIncludes'; +import NodeRenderer from './NodeRenderer'; + +type ContentCollectionProviderProps = { + nodeName?: string; + children: ({ + collectionProps, + children, + }: { + collectionProps: Record; + children: React.ReactNode; + }) => React.ReactNode; +}; + +const ContentCollectionProvider = async ({ nodeName, children }: ContentCollectionProviderProps) => { + const inBackend = useInBackend(); + const { collectionNode, collectionProps } = await useContentCollection(nodeName)(); + + if (!collectionNode) { + return null; + } + + return ( + <> + {children({ + collectionProps, + children: ( + <> + {collectionNode.children?.map((child) => ( + + ))} + {inBackend && ( + + )} + + ), + })} + + ); +}; + +export default ContentCollectionProvider; diff --git a/src/server/components/ContentComponent.tsx b/src/server/components/ContentComponent.tsx index 40b0672..20396fc 100644 --- a/src/server/components/ContentComponent.tsx +++ b/src/server/components/ContentComponent.tsx @@ -1,5 +1,4 @@ -import { useInBackend, useNode } from '../utils/hooks'; -import ContentComponentIncludes from './client/ContentComponentIncludes'; +import ContentComponentProvider from './ContentComponentProvider'; type ContentComponentProps = { as?: keyof JSX.IntrinsicElements; @@ -8,29 +7,17 @@ type ContentComponentProps = { }; const ContentComponent = async ({ as = 'div', children, ...rest }: ContentComponentProps) => { - const inBackend = useInBackend(); - const loadNode = useNode(); - const node = await loadNode(); - - if (!node) { - return null; - } - const Component = as; return ( - - {children} - {inBackend && ( - + + {({ componentProps, includes }) => ( + + {children} + {includes} + )} - + ); }; diff --git a/src/server/components/ContentComponentProvider.tsx b/src/server/components/ContentComponentProvider.tsx new file mode 100644 index 0000000..061f9c8 --- /dev/null +++ b/src/server/components/ContentComponentProvider.tsx @@ -0,0 +1,38 @@ +import { useInBackend } from '../utils/hooks'; +import { useContentComponent } from '../utils/hooks'; +import ContentComponentIncludes from './client/ContentComponentIncludes'; + +type ContentComponentProviderProps = { + children: ({ + componentProps, + includes, + }: { + componentProps: Record; + includes: React.ReactNode; + }) => React.ReactNode; +}; + +const ContentComponentProvider = async ({ children }: ContentComponentProviderProps) => { + const inBackend = useInBackend(); + const { componentNode, componentProps } = await useContentComponent()(); + + if (!componentNode) { + return null; + } + + return ( + <> + {children({ + componentProps, + includes: inBackend && ( + + ), + })} + + ); +}; + +export default ContentComponentProvider; diff --git a/src/server/utils/hooks.ts b/src/server/utils/hooks.ts index a61ece8..94db862 100644 --- a/src/server/utils/hooks.ts +++ b/src/server/utils/hooks.ts @@ -60,7 +60,7 @@ export const useSiteNode = () => { export const useInBackend = () => { const neosContext = useContext(NeosServerContext); - return neosContext?.inBackend; + return !!neosContext?.inBackend; }; export const useEditPreviewMode = () => { @@ -72,3 +72,66 @@ export const useEditPreviewMode = () => { return neosData?.backend?.editPreviewMode; }; }; + +export const useContentCollection = (nodeName?: string) => { + const inBackend = useInBackend(); + const neosContext = useContext(NeosServerContext); + + return async () => { + const neosData = inBackend + ? await loadPreviewDocumentPropsCached(neosContext.contextNodePath) + : await loadDocumentPropsCached(neosContext.routePath); + + if (!neosData) { + return { + collectionNode: undefined, + collectionProps: {}, + }; + } + + const currentNode = resolveCurrentNode(neosContext, neosData); + const collectionNode = nodeName ? currentNode?.children?.find((child) => child.nodeName === nodeName) : currentNode; + + if (!collectionNode) { + return { + collectionNode: undefined, + collectionProps: {}, + }; + } + + return { + collectionNode, + collectionProps: { + 'data-__neos-node-contextpath': inBackend ? collectionNode.contextPath : undefined, + // Use a fixed fusion path to render an out-of-band preview of this node + 'data-__neos-fusion-path': inBackend ? 'neosNext/previewNode' : undefined, + 'data-__neos-insertion-anchor': inBackend ? true : undefined, + }, + }; + }; +}; + +export const useContentComponent = () => { + const inBackend = useInBackend(); + + return async () => { + const node = await useNode()(); + + if (!node) { + return { + componentNode: undefined, + componentProps: {}, + }; + } + + return { + componentNode: node, + componentProps: { + 'data-__neos-node-contextpath': inBackend ? node.contextPath : undefined, + // Use a fixed fusion path to render an out-of-band preview of this node. + // The Networkteam.Neos.Next package provides a Fusion prototype that renders the node through Next.js. + 'data-__neos-fusion-path': inBackend ? 'neosNext/previewNode' : undefined, + }, + }; + }; +}; From 303f28cf5bb6ef1e8cf15dae32d9fabb4662b1cb Mon Sep 17 00:00:00 2001 From: Philip Schmidt Date: Wed, 29 Nov 2023 12:34:41 +0100 Subject: [PATCH 09/39] client side backend container --- src/client.ts | 1 + src/server.ts | 1 - src/server/components/client/BackendContainer.tsx | 9 +++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 src/server/components/client/BackendContainer.tsx diff --git a/src/client.ts b/src/client.ts index b84aed6..3942732 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,3 +1,4 @@ 'use client'; +export { default as BackendContainer } from './server/components/client/BackendContainer'; export { default as BackendIncludes } from './server/components/client/BackendIncludes'; diff --git a/src/server.ts b/src/server.ts index cf3c9fc..aac740e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,4 +1,3 @@ -export { default as BackendContainer } from './lib/components/BackendContainer'; export { default as ContentCollection } from './server/components/ContentCollection'; export { default as ContentCollectionProvider } from './server/components/ContentCollectionProvider'; export { default as ContentComponent } from './server/components/ContentComponent'; diff --git a/src/server/components/client/BackendContainer.tsx b/src/server/components/client/BackendContainer.tsx new file mode 100644 index 0000000..501e51c --- /dev/null +++ b/src/server/components/client/BackendContainer.tsx @@ -0,0 +1,9 @@ +'use client'; + +export default function BackendContainer() { + /* + This container needs to be present in the DOM on page load for the Neos UI to work. + Adding it via useEffect only for preview didn't show it correctly until changing the UI. + */ + return
; +} From f360957e4d39ba83d0da68e3aa5cec567729677b Mon Sep 17 00:00:00 2001 From: Philip Schmidt Date: Wed, 29 Nov 2023 14:38:52 +0100 Subject: [PATCH 10/39] prevent hydration errors caused by backend container --- src/server/components/client/BackendContainer.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/server/components/client/BackendContainer.tsx b/src/server/components/client/BackendContainer.tsx index 501e51c..f9c7826 100644 --- a/src/server/components/client/BackendContainer.tsx +++ b/src/server/components/client/BackendContainer.tsx @@ -3,7 +3,9 @@ export default function BackendContainer() { /* This container needs to be present in the DOM on page load for the Neos UI to work. - Adding it via useEffect only for preview didn't show it correctly until changing the UI. + To prevent hydration errors, we're using dangerouslySetInnerHTML in combination with suppressHydrationWarning for now. */ - return
; + return ( +
' }} suppressHydrationWarning /> + ); } From b3612ed6ad557992514a437deb953b9116fa4ea9 Mon Sep 17 00:00:00 2001 From: Philip Schmidt Date: Fri, 12 Jan 2024 14:18:30 +0100 Subject: [PATCH 11/39] get rid of server context --- package.json | 2 +- src/server.ts | 1 - src/server/components/ContentCollection.tsx | 9 ++- .../components/ContentCollectionProvider.tsx | 11 ++- src/server/components/ContentComponent.tsx | 7 +- .../components/ContentComponentProvider.tsx | 9 ++- src/server/components/Editable.tsx | 11 ++- src/server/components/NodeRenderer.tsx | 29 ++++---- src/server/utils/context.ts | 5 -- src/server/utils/helper.ts | 11 +-- src/server/utils/hooks.ts | 73 ++++++++----------- src/server/utils/nodeTypes.ts | 4 +- src/types/index.ts | 2 +- tsconfig.dev.json | 1 - 14 files changed, 83 insertions(+), 92 deletions(-) delete mode 100644 src/server/utils/context.ts diff --git a/package.json b/package.json index 1baacf7..5cb5f12 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "build": "yarn clean && yarn tsc --project tsconfig.json && yarn babel --config-file ./config/babel.config.js src --out-dir . --extensions \".tsx,.ts,.js,.jsx\"", "clean": "rm -rf index.d.ts index.d.ts.map index.js server.d.ts server.d.ts.map server.js client.d.ts client.d.ts.map client.js api lib types utils server", "dev": "yarn clean && yarn watch", - "watch": "yarn tsc --project tsconfig.dev.json", + "watch": "yarn tsc --project tsconfig.dev.json --watch", "lint": "eslint src config" }, "dependencies": { diff --git a/src/server.ts b/src/server.ts index aac740e..dcca482 100644 --- a/src/server.ts +++ b/src/server.ts @@ -4,7 +4,6 @@ export { default as ContentComponent } from './server/components/ContentComponen export { default as ContentComponentProvider } from './server/components/ContentComponentProvider'; export { default as Editable } from './server/components/Editable'; export { default as NodeRenderer } from './server/components/NodeRenderer'; -export { NeosServerContext } from './server/utils/context'; export { loadDocumentProps, loadDocumentPropsCached, diff --git a/src/server/components/ContentCollection.tsx b/src/server/components/ContentCollection.tsx index 948999c..0a33e61 100644 --- a/src/server/components/ContentCollection.tsx +++ b/src/server/components/ContentCollection.tsx @@ -1,20 +1,23 @@ +import { ContextProps } from 'src/types'; + import { useInBackend } from '../utils/hooks'; import ContentCollectionProvider from './ContentCollectionProvider'; type ContentCollectionProps = { + ctx: ContextProps; as?: keyof JSX.IntrinsicElements; nodeName?: string; [x: string]: any; }; -const ContentCollection = async ({ as = 'div', nodeName, ...rest }: ContentCollectionProps) => { - const inBackend = useInBackend(); +const ContentCollection = async ({ ctx, as = 'div', nodeName, ...rest }: ContentCollectionProps) => { + const inBackend = useInBackend(ctx); const { className, ...restAttributes } = rest; const Component = as; return ( - + {({ collectionProps, children }) => ( React.ReactNode; }; -const ContentCollectionProvider = async ({ nodeName, children }: ContentCollectionProviderProps) => { - const inBackend = useInBackend(); - const { collectionNode, collectionProps } = await useContentCollection(nodeName)(); +const ContentCollectionProvider = async ({ ctx, nodeName, children }: ContentCollectionProviderProps) => { + const inBackend = useInBackend(ctx); + const { collectionNode, collectionProps } = await useContentCollection(ctx, nodeName)(); if (!collectionNode) { return null; @@ -29,7 +32,7 @@ const ContentCollectionProvider = async ({ nodeName, children }: ContentCollecti children: ( <> {collectionNode.children?.map((child) => ( - + ))} {inBackend && ( { +const ContentComponent = async ({ ctx, as = 'div', children, ...rest }: ContentComponentProps) => { const Component = as; return ( - + {({ componentProps, includes }) => ( {children} diff --git a/src/server/components/ContentComponentProvider.tsx b/src/server/components/ContentComponentProvider.tsx index 061f9c8..f559c6e 100644 --- a/src/server/components/ContentComponentProvider.tsx +++ b/src/server/components/ContentComponentProvider.tsx @@ -1,8 +1,11 @@ +import { ContextProps } from 'src/types'; + import { useInBackend } from '../utils/hooks'; import { useContentComponent } from '../utils/hooks'; import ContentComponentIncludes from './client/ContentComponentIncludes'; type ContentComponentProviderProps = { + ctx: ContextProps; children: ({ componentProps, includes, @@ -12,9 +15,9 @@ type ContentComponentProviderProps = { }) => React.ReactNode; }; -const ContentComponentProvider = async ({ children }: ContentComponentProviderProps) => { - const inBackend = useInBackend(); - const { componentNode, componentProps } = await useContentComponent()(); +const ContentComponentProvider = async ({ ctx, children }: ContentComponentProviderProps) => { + const inBackend = useInBackend(ctx); + const { componentNode, componentProps } = await useContentComponent(ctx)(); if (!componentNode) { return null; diff --git a/src/server/components/Editable.tsx b/src/server/components/Editable.tsx index e23bc0e..066adfa 100644 --- a/src/server/components/Editable.tsx +++ b/src/server/components/Editable.tsx @@ -1,15 +1,18 @@ +import { ContextProps } from 'src/types'; + import { useEditPreviewMode, useInBackend, useNode } from '../utils/hooks'; type EditableProps = { + ctx: ContextProps; as?: keyof JSX.IntrinsicElements; property: string; [x: string]: any; }; -const Editable = async ({ as = 'div', property, ...rest }: EditableProps) => { - const inBackend = useInBackend(); - const loadNode = useNode(); - const loadEditPreviewMode = useEditPreviewMode(); +const Editable = async ({ ctx, as = 'div', property, ...rest }: EditableProps) => { + const inBackend = useInBackend(ctx); + const loadNode = useNode(ctx); + const loadEditPreviewMode = useEditPreviewMode(ctx); const node = await loadNode(); const editPreviewMode = await loadEditPreviewMode(); diff --git a/src/server/components/NodeRenderer.tsx b/src/server/components/NodeRenderer.tsx index 6a65d1d..f4281bc 100644 --- a/src/server/components/NodeRenderer.tsx +++ b/src/server/components/NodeRenderer.tsx @@ -1,17 +1,17 @@ -import { useContext } from 'react'; - -import { NeosContentNode } from '../../types'; -import { NeosServerContext } from '../utils/context'; +import { ContextProps, NeosContentNode } from '../../types'; import { loadDocumentPropsCached, loadPreviewDocumentPropsCached } from '../utils/dataLoader'; import { getNodeType } from '../utils/nodeTypes'; -const NodeRenderer = async ({ node }: { node: NeosContentNode }) => { - const neosContext = useContext(NeosServerContext); +type NodeRendererProps = { + ctx: ContextProps; + node: NeosContentNode; +}; +const NodeRenderer = async ({ ctx, node }: NodeRendererProps) => { // We just fetch again and hope it will be cached - const neosData = neosContext.inBackend - ? await loadPreviewDocumentPropsCached(neosContext.contextNodePath) - : await loadDocumentPropsCached(neosContext.routePath); + const neosData = ctx.inBackend + ? await loadPreviewDocumentPropsCached(ctx.contextNodePath) + : await loadDocumentPropsCached(ctx.routePath); if (!neosData) { return
Could not load data
; @@ -24,14 +24,13 @@ const NodeRenderer = async ({ node }: { node: NeosContentNode }) => { } return ( - - - + /> ); }; diff --git a/src/server/utils/context.ts b/src/server/utils/context.ts deleted file mode 100644 index 97d9f38..0000000 --- a/src/server/utils/context.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createServerContext } from 'react'; - -import { NeosServerContextProps } from '../../types'; - -export const NeosServerContext = createServerContext('neosDataContext', {}); diff --git a/src/server/utils/helper.ts b/src/server/utils/helper.ts index 928e7a8..5ff6f0d 100644 --- a/src/server/utils/helper.ts +++ b/src/server/utils/helper.ts @@ -1,17 +1,14 @@ -import { NeosContentNode, NeosData, NeosServerContextProps } from '../../types'; +import { ContextProps, NeosContentNode, NeosData } from '../../types'; -export function resolveCurrentNode( - neosContext: NeosServerContextProps, - neosData: NeosData -): NeosContentNode | undefined { +export function resolveCurrentNode(ctx: ContextProps, neosData: NeosData): NeosContentNode | undefined { // Recurse into neosData.node / .children to find a node where identifier == currentNodeIdentifier // and return that node - if (!neosContext.currentNodeIdentifier) { + if (!ctx.currentNodeIdentifier) { return undefined; } - return resolveCurrentNodeRecursive(neosContext.currentNodeIdentifier, neosData.node); + return resolveCurrentNodeRecursive(ctx.currentNodeIdentifier, neosData.node); } export function resolveCurrentNodeRecursive( diff --git a/src/server/utils/hooks.ts b/src/server/utils/hooks.ts index 94db862..46890c5 100644 --- a/src/server/utils/hooks.ts +++ b/src/server/utils/hooks.ts @@ -1,86 +1,73 @@ -import { useContext } from 'react'; +import { ContextProps } from 'src/types'; -import { NeosServerContext } from './context'; import { loadDocumentPropsCached, loadPreviewDocumentPropsCached } from './dataLoader'; import { resolveCurrentNode } from './helper'; -export const useMeta = () => { - const neosContext = useContext(NeosServerContext); - +export const useMeta = (ctx: ContextProps) => { return async () => { - const neosData = neosContext.inBackend - ? await loadPreviewDocumentPropsCached(neosContext.contextNodePath) - : await loadDocumentPropsCached(neosContext.routePath); + const neosData = ctx.inBackend + ? await loadPreviewDocumentPropsCached(ctx.contextNodePath) + : await loadDocumentPropsCached(ctx.routePath); return neosData?.meta; }; }; -export const useNode = () => { - const neosContext = useContext(NeosServerContext); - +export const useNode = (ctx: ContextProps) => { return async () => { - const neosData = neosContext.inBackend - ? await loadPreviewDocumentPropsCached(neosContext.contextNodePath) - : await loadDocumentPropsCached(neosContext.routePath); + const neosData = ctx.inBackend + ? await loadPreviewDocumentPropsCached(ctx.contextNodePath) + : await loadDocumentPropsCached(ctx.routePath); if (!neosData) { return undefined; } - const node = resolveCurrentNode(neosContext, neosData); + const node = resolveCurrentNode(ctx, neosData); return node; }; }; -export const useDocumentNode = () => { - const neosContext = useContext(NeosServerContext); - +export const useDocumentNode = (ctx: ContextProps) => { return async () => { - const neosData = neosContext.inBackend - ? await loadPreviewDocumentPropsCached(neosContext.contextNodePath) - : await loadDocumentPropsCached(neosContext.routePath); + const neosData = ctx.inBackend + ? await loadPreviewDocumentPropsCached(ctx.contextNodePath) + : await loadDocumentPropsCached(ctx.routePath); return neosData?.node; }; }; -export const useSiteNode = () => { - const neosContext = useContext(NeosServerContext); - +export const useSiteNode = (ctx: ContextProps) => { return async () => { - const neosData = neosContext.inBackend - ? await loadPreviewDocumentPropsCached(neosContext.contextNodePath) - : await loadDocumentPropsCached(neosContext.routePath); + const neosData = ctx.inBackend + ? await loadPreviewDocumentPropsCached(ctx.contextNodePath) + : await loadDocumentPropsCached(ctx.routePath); return neosData?.site; }; }; -export const useInBackend = () => { - const neosContext = useContext(NeosServerContext); - return !!neosContext?.inBackend; +export const useInBackend = (ctx: ContextProps) => { + return !!ctx?.inBackend; }; -export const useEditPreviewMode = () => { - const neosContext = useContext(NeosServerContext); - +export const useEditPreviewMode = (ctx: ContextProps) => { return async () => { - const neosData = await loadPreviewDocumentPropsCached(neosContext.contextNodePath); + const neosData = await loadPreviewDocumentPropsCached(ctx.contextNodePath); return neosData?.backend?.editPreviewMode; }; }; -export const useContentCollection = (nodeName?: string) => { - const inBackend = useInBackend(); - const neosContext = useContext(NeosServerContext); +export const useContentCollection = (ctx: ContextProps, nodeName?: string) => { + const inBackend = useInBackend(ctx); return async () => { const neosData = inBackend - ? await loadPreviewDocumentPropsCached(neosContext.contextNodePath) - : await loadDocumentPropsCached(neosContext.routePath); + ? await loadPreviewDocumentPropsCached(ctx.contextNodePath) + : await loadDocumentPropsCached(ctx.routePath); if (!neosData) { return { @@ -89,7 +76,7 @@ export const useContentCollection = (nodeName?: string) => { }; } - const currentNode = resolveCurrentNode(neosContext, neosData); + const currentNode = resolveCurrentNode(ctx, neosData); const collectionNode = nodeName ? currentNode?.children?.find((child) => child.nodeName === nodeName) : currentNode; if (!collectionNode) { @@ -111,11 +98,11 @@ export const useContentCollection = (nodeName?: string) => { }; }; -export const useContentComponent = () => { - const inBackend = useInBackend(); +export const useContentComponent = (ctx: ContextProps) => { + const inBackend = useInBackend(ctx); return async () => { - const node = await useNode()(); + const node = await useNode(ctx)(); if (!node) { return { diff --git a/src/server/utils/nodeTypes.ts b/src/server/utils/nodeTypes.ts index 6a50e1f..ac1f6e7 100644 --- a/src/server/utils/nodeTypes.ts +++ b/src/server/utils/nodeTypes.ts @@ -1,4 +1,4 @@ -import { NeosNodeTypes } from '../../types'; +import { ContextProps, NeosNodeTypes } from '../../types'; let nodeTypes: NeosNodeTypes; @@ -6,6 +6,6 @@ export function initNodeTypes(data: NeosNodeTypes) { nodeTypes = data; } -export function getNodeType(nodeTypeName: string): React.FC | undefined { +export function getNodeType(nodeTypeName: string): React.FC<{ ctx: ContextProps }> | undefined { return nodeTypes?.[nodeTypeName]; } diff --git a/src/types/index.ts b/src/types/index.ts index 291d66a..f922eee 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -114,7 +114,7 @@ export interface ApiErrors { errors?: { message: string; code: number }[]; } -export type NeosServerContextProps = { +export type ContextProps = { routePath?: string; contextNodePath?: string; inBackend?: boolean; diff --git a/tsconfig.dev.json b/tsconfig.dev.json index 3b99025..b696ac1 100644 --- a/tsconfig.dev.json +++ b/tsconfig.dev.json @@ -1,7 +1,6 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "watch": true, "emitDeclarationOnly": false }, "watchOptions": { From 63cdea094337fc24f594d8c4ff101e1aa550ab18 Mon Sep 17 00:00:00 2001 From: Philip Schmidt Date: Sat, 13 Jan 2024 16:44:15 +0100 Subject: [PATCH 12/39] task: add ctx prop to NeosNodeType type --- src/types/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/index.ts b/src/types/index.ts index f922eee..f49f8d6 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -80,7 +80,7 @@ export interface BackendEditPreviewMode { options: Record | null; } -export type NeosNodeTypes = Record; +export type NeosNodeTypes = Record>; export interface NeosContextProps { node: NeosContentNode; From 11d54b449a737408c78d4075b3f93e1bb2bde388 Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Thu, 8 Jun 2023 14:46:57 +0200 Subject: [PATCH 13/39] Remove note about .mjs, require still works --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0852efd..e095f32 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ We also published some supporting tools: * Create or use an existing Next.js project * Add `@networkteam/zebra` to your project -* Apply `withZebra` to your `next.config.mjs` (we only expose an ESM module, so the Next.js config needs to use the `.mjs` extension): +* Apply `withZebra` to your `next.config.mjs`: ```js import { withZebra } from '@networkteam/zebra'; From 56444091d943c2df7d91e977153e26ec079655b2 Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Tue, 23 Jan 2024 18:15:36 +0100 Subject: [PATCH 14/39] Add peer package compatibility with Next.js 14 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5cb5f12..79f373d 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "typescript": "^5.1.0" }, "peerDependencies": { - "next": "^12.2.0 || ^13.0.0", + "next": "^12.2.0 || ^13.0.0 || ^14.0.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, From 8f40335dad7408c5cb4de694da6242ddf36b0a50 Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Tue, 23 Jan 2024 19:17:16 +0100 Subject: [PATCH 15/39] Add data loader options and throw on errors --- src/server/utils/dataLoader.ts | 44 +++++++++++++++++++++------------- src/server/utils/hooks.ts | 40 +++++++++++++++---------------- src/types/index.ts | 17 +++++++++++++ 3 files changed, 64 insertions(+), 37 deletions(-) diff --git a/src/server/utils/dataLoader.ts b/src/server/utils/dataLoader.ts index 5dfe2ed..823d76d 100644 --- a/src/server/utils/dataLoader.ts +++ b/src/server/utils/dataLoader.ts @@ -2,13 +2,15 @@ import log from 'loglevel'; import { headers as nextHeaders } from 'next/headers'; import { cache } from 'react'; -import { ApiErrors, NeosData, SiteData } from '../../types'; +import { ApiErrors, DataLoaderOptions, NeosData, SiteData } from '../../types'; log.setDefaultLevel(log.levels.DEBUG); -// TODO Add explicit cache configuration for cached / uncached -export const loadDocumentProps = async (params: { slug: string | string[] }) => { +export const loadDocumentProps = async (params: { slug: string | string[] }, opts?: DataLoaderOptions) => { const apiUrl = process.env.NEOS_BASE_URL; + if (!apiUrl && opts?.optional) { + return undefined; + } if (!apiUrl) { throw new Error('Missing NEOS_BASE_URL environment variable'); } @@ -28,7 +30,8 @@ export const loadDocumentProps = async (params: { slug: string | string[] }) => const response = await fetch(fetchUrl, { headers: buildNeosHeaders(), - cache: 'no-store', + cache: opts?.cache ?? 'no-store', + next: opts?.next, }); if (!response.ok) { @@ -42,8 +45,7 @@ export const loadDocumentProps = async (params: { slug: string | string[] }) => if (data.errors) { const flatErrors = data.errors.map((e) => e.message).join(', '); log.error('error fetching from content API with url', fetchUrl, ':', flatErrors); - - return undefined; + throw new Error('Content API responded with error: ' + flatErrors); } } @@ -54,8 +56,14 @@ export const loadDocumentProps = async (params: { slug: string | string[] }) => return data; }; -export const loadPreviewDocumentProps = async (searchParams: { [key: string]: string | string[] | undefined }) => { +export const loadPreviewDocumentProps = async ( + searchParams: { [key: string]: string | string[] | undefined }, + opts?: DataLoaderOptions +) => { const apiUrl = process.env.NEOS_BASE_URL; + if (!apiUrl && opts?.optional) { + return undefined; + } if (!apiUrl) { throw new Error('Missing NEOS_BASE_URL environment variable'); } @@ -83,8 +91,7 @@ export const loadPreviewDocumentProps = async (searchParams: { [key: string]: st if (data.errors) { const flatErrors = data.errors.map((e) => e.message).join(', '); log.error('error fetching from content API with url', fetchUrl, ':', flatErrors); - - return undefined; + throw new Error('Content API responded with error: ' + flatErrors); } } @@ -95,22 +102,22 @@ export const loadPreviewDocumentProps = async (searchParams: { [key: string]: st return data; }; -export const loadDocumentPropsCached = cache((routePath: string | undefined) => { +export const loadDocumentPropsCached = cache((routePath: string | undefined, opts?: DataLoaderOptions) => { if (!routePath) { return undefined; } log.debug('fetching data from Neos inside cache with route path', routePath); const slug = routePath.split('/').filter((s) => s.length > 0); - return loadDocumentProps({ slug }); + return loadDocumentProps({ slug }, opts); }); -export const loadPreviewDocumentPropsCached = cache((contextNodePath: string | undefined) => { +export const loadPreviewDocumentPropsCached = cache((contextNodePath: string | undefined, opts?: DataLoaderOptions) => { if (!contextNodePath) { return undefined; } log.debug('fetching data from Neos inside cache with context node path', contextNodePath); const searchParams = { 'node[__contextNodePath]': contextNodePath }; - return loadPreviewDocumentProps(searchParams); + return loadPreviewDocumentProps(searchParams, opts); }); export const buildNeosHeaders = () => { @@ -124,8 +131,11 @@ export const buildNeosHeaders = () => { return headers; }; -export const loadSiteProps = async () => { +export const loadSiteProps = async (opts?: DataLoaderOptions) => { const apiUrl = process.env.NEOS_BASE_URL; + if (!apiUrl && opts?.optional) { + return undefined; + } if (!apiUrl) { throw new Error('Missing NEOS_BASE_URL environment variable'); } @@ -137,7 +147,8 @@ export const loadSiteProps = async () => { const response = await fetch(fetchUrl, { headers: buildNeosHeaders(), - cache: 'no-store', + cache: opts?.cache ?? 'no-store', + next: opts?.next, }); if (!response.ok) { @@ -145,8 +156,7 @@ export const loadSiteProps = async () => { if (data.errors) { const flatErrors = data.errors.map((e) => e.message).join(', '); log.error('error fetching from content API with url', fetchUrl, ':', flatErrors); - - return undefined; + throw new Error('Content API responded with error: ' + flatErrors); } } diff --git a/src/server/utils/hooks.ts b/src/server/utils/hooks.ts index 46890c5..7bc981a 100644 --- a/src/server/utils/hooks.ts +++ b/src/server/utils/hooks.ts @@ -1,23 +1,23 @@ -import { ContextProps } from 'src/types'; +import { ContextProps, DataLoaderOptions } from 'src/types'; import { loadDocumentPropsCached, loadPreviewDocumentPropsCached } from './dataLoader'; import { resolveCurrentNode } from './helper'; -export const useMeta = (ctx: ContextProps) => { +export const useMeta = (ctx: ContextProps, opts?: DataLoaderOptions) => { return async () => { const neosData = ctx.inBackend - ? await loadPreviewDocumentPropsCached(ctx.contextNodePath) - : await loadDocumentPropsCached(ctx.routePath); + ? await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts) + : await loadDocumentPropsCached(ctx.routePath, opts); return neosData?.meta; }; }; -export const useNode = (ctx: ContextProps) => { +export const useNode = (ctx: ContextProps, opts?: DataLoaderOptions) => { return async () => { const neosData = ctx.inBackend - ? await loadPreviewDocumentPropsCached(ctx.contextNodePath) - : await loadDocumentPropsCached(ctx.routePath); + ? await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts) + : await loadDocumentPropsCached(ctx.routePath, opts); if (!neosData) { return undefined; @@ -29,21 +29,21 @@ export const useNode = (ctx: ContextProps) => { }; }; -export const useDocumentNode = (ctx: ContextProps) => { +export const useDocumentNode = (ctx: ContextProps, opts?: DataLoaderOptions) => { return async () => { const neosData = ctx.inBackend - ? await loadPreviewDocumentPropsCached(ctx.contextNodePath) - : await loadDocumentPropsCached(ctx.routePath); + ? await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts) + : await loadDocumentPropsCached(ctx.routePath, opts); return neosData?.node; }; }; -export const useSiteNode = (ctx: ContextProps) => { +export const useSiteNode = (ctx: ContextProps, opts?: DataLoaderOptions) => { return async () => { const neosData = ctx.inBackend - ? await loadPreviewDocumentPropsCached(ctx.contextNodePath) - : await loadDocumentPropsCached(ctx.routePath); + ? await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts) + : await loadDocumentPropsCached(ctx.routePath, opts); return neosData?.site; }; @@ -53,21 +53,21 @@ export const useInBackend = (ctx: ContextProps) => { return !!ctx?.inBackend; }; -export const useEditPreviewMode = (ctx: ContextProps) => { +export const useEditPreviewMode = (ctx: ContextProps, opts?: DataLoaderOptions) => { return async () => { - const neosData = await loadPreviewDocumentPropsCached(ctx.contextNodePath); + const neosData = await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts); return neosData?.backend?.editPreviewMode; }; }; -export const useContentCollection = (ctx: ContextProps, nodeName?: string) => { +export const useContentCollection = (ctx: ContextProps, nodeName?: string, opts?: DataLoaderOptions) => { const inBackend = useInBackend(ctx); return async () => { const neosData = inBackend - ? await loadPreviewDocumentPropsCached(ctx.contextNodePath) - : await loadDocumentPropsCached(ctx.routePath); + ? await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts) + : await loadDocumentPropsCached(ctx.routePath, opts); if (!neosData) { return { @@ -98,11 +98,11 @@ export const useContentCollection = (ctx: ContextProps, nodeName?: string) => { }; }; -export const useContentComponent = (ctx: ContextProps) => { +export const useContentComponent = (ctx: ContextProps, opts?: DataLoaderOptions) => { const inBackend = useInBackend(ctx); return async () => { - const node = await useNode(ctx)(); + const node = await useNode(ctx, opts)(); if (!node) { return { diff --git a/src/types/index.ts b/src/types/index.ts index f49f8d6..f495fc9 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -121,3 +121,20 @@ export type ContextProps = { documentNodeIdentifier?: string; currentNodeIdentifier?: string; }; + +export type DataLoaderOptions = { + /** + * If true, the data loader will not throw an error if the content API base URL is not set and no data can be fetched. + */ + optional?: boolean; + + /** + * The fetch request cache mode to use. + */ + cache?: RequestCache; + + /** + * The Next fetch request config for revalidation and tags. + */ + next?: NextFetchRequestConfig; +}; From 93a2188bf9d4a6cea73b7ce5e3753439dd09e9aa Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Tue, 23 Jan 2024 19:18:29 +0100 Subject: [PATCH 16/39] Add unit tests for loadDocumentProps with Vitest --- .gitignore | 16 +- package.json | 10 +- test/server/utils/dataLoader.test.ts | 49 ++ test/test-setup.ts | 13 + vitest.config.js | 8 + yarn.lock | 666 ++++++++++++++++++++++++++- 6 files changed, 737 insertions(+), 25 deletions(-) create mode 100644 test/server/utils/dataLoader.test.ts create mode 100644 test/test-setup.ts create mode 100644 vitest.config.js diff --git a/.gitignore b/.gitignore index f5cd7d9..840c493 100644 --- a/.gitignore +++ b/.gitignore @@ -12,14 +12,14 @@ dist dist-ssr *.local -api/ -lib/ -types/ -utils/ -server/ -index.* -server.* -client.* +/api/ +/lib/ +/types/ +/utils/ +/server/ +/index.* +/server.* +/client.* !/src/**/* diff --git a/package.json b/package.json index 79f373d..b373ede 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "clean": "rm -rf index.d.ts index.d.ts.map index.js server.d.ts server.d.ts.map server.js client.d.ts client.d.ts.map client.js api lib types utils server", "dev": "yarn clean && yarn watch", "watch": "yarn tsc --project tsconfig.dev.json --watch", - "lint": "eslint src config" + "lint": "eslint src config", + "test": "vitest" }, "dependencies": { "@babel/runtime": "^7.20.13", @@ -49,9 +50,10 @@ "eslint-plugin-simple-import-sort": "^8.0.0", "next": "^13.4.0", "prettier": "^2.7.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "typescript": "^5.1.0" + "react": "18.3.0-canary-b2d637128-20240123", + "react-dom": "18.3.0-canary-b2d637128-20240123", + "typescript": "^5.1.0", + "vitest": "^1.2.1" }, "peerDependencies": { "next": "^12.2.0 || ^13.0.0 || ^14.0.0", diff --git a/test/server/utils/dataLoader.test.ts b/test/server/utils/dataLoader.test.ts new file mode 100644 index 0000000..124def9 --- /dev/null +++ b/test/server/utils/dataLoader.test.ts @@ -0,0 +1,49 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; + +import { loadDocumentProps } from '../../../src/server'; + +describe('loadDocumentProps', () => { + describe('without options', () => { + test('should throw an error if NEOS_BASE_URL is not set', async () => { + await expect(loadDocumentProps({ slug: 'foo' })).rejects.toThrowError( + 'Missing NEOS_BASE_URL environment variable' + ); + }); + + describe('with NEOS_BASE_URL set', () => { + beforeEach(() => { + vi.stubEnv('NEOS_BASE_URL', 'http://neos:1234'); + }); + + test('should fetch from configured API', async () => { + const fetch = vi.fn().mockResolvedValue(createOkayFetchResponse({ meta: { title: 'Foo' } })); + vi.stubGlobal('fetch', fetch); + + await expect(loadDocumentProps({ slug: 'foo' })).resolves.toStrictEqual({ + meta: { title: 'Foo' }, + }); + + expect(fetch).toHaveBeenCalledWith('http://neos:1234/neos/content-api/document?path=%2Ffoo', { + cache: 'no-store', + headers: {}, + next: undefined, + }); + }); + }); + }); + + describe('with optional option', () => { + test('should not throw an error if NEOS_BASE_URL is not set', async () => { + await expect(loadDocumentProps({ slug: 'foo' }, { optional: true })).resolves.toBeUndefined(); + }); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + vi.unstubAllGlobals(); + }); +}); + +function createOkayFetchResponse(data: any) { + return { ok: true, status: 200, json: () => new Promise((resolve) => resolve(data)) }; +} diff --git a/test/test-setup.ts b/test/test-setup.ts new file mode 100644 index 0000000..7a06a64 --- /dev/null +++ b/test/test-setup.ts @@ -0,0 +1,13 @@ +import { afterAll, afterEach, beforeAll, beforeEach, vi } from 'vitest'; + +beforeAll(() => {}); + +afterAll(() => {}); + +beforeEach(() => { + vi.useFakeTimers(); +}); + +afterEach(() => { + vi.useRealTimers(); +}); diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 0000000..5059b9c --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,8 @@ +import { defineConfig } from 'vite'; + +// https://vitejs.dev/config/ +export default defineConfig({ + test: { + setupFiles: ['./test/test-setup.ts'], + }, +}); diff --git a/yarn.lock b/yarn.lock index 56e1aaf..7644842 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1151,6 +1151,121 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@esbuild/aix-ppc64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz#2acd20be6d4f0458bc8c784103495ff24f13b1d3" + integrity sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g== + +"@esbuild/android-arm64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz#b45d000017385c9051a4f03e17078abb935be220" + integrity sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q== + +"@esbuild/android-arm@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.11.tgz#f46f55414e1c3614ac682b29977792131238164c" + integrity sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw== + +"@esbuild/android-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.11.tgz#bfc01e91740b82011ef503c48f548950824922b2" + integrity sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg== + +"@esbuild/darwin-arm64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz#533fb7f5a08c37121d82c66198263dcc1bed29bf" + integrity sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ== + +"@esbuild/darwin-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz#62f3819eff7e4ddc656b7c6815a31cf9a1e7d98e" + integrity sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g== + +"@esbuild/freebsd-arm64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz#d478b4195aa3ca44160272dab85ef8baf4175b4a" + integrity sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA== + +"@esbuild/freebsd-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz#7bdcc1917409178257ca6a1a27fe06e797ec18a2" + integrity sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw== + +"@esbuild/linux-arm64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz#58ad4ff11685fcc735d7ff4ca759ab18fcfe4545" + integrity sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg== + +"@esbuild/linux-arm@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz#ce82246d873b5534d34de1e5c1b33026f35e60e3" + integrity sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q== + +"@esbuild/linux-ia32@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz#cbae1f313209affc74b80f4390c4c35c6ab83fa4" + integrity sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA== + +"@esbuild/linux-loong64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz#5f32aead1c3ec8f4cccdb7ed08b166224d4e9121" + integrity sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg== + +"@esbuild/linux-mips64el@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz#38eecf1cbb8c36a616261de858b3c10d03419af9" + integrity sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg== + +"@esbuild/linux-ppc64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz#9c5725a94e6ec15b93195e5a6afb821628afd912" + integrity sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA== + +"@esbuild/linux-riscv64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz#2dc4486d474a2a62bbe5870522a9a600e2acb916" + integrity sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ== + +"@esbuild/linux-s390x@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz#4ad8567df48f7dd4c71ec5b1753b6f37561a65a8" + integrity sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q== + +"@esbuild/linux-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz#b7390c4d5184f203ebe7ddaedf073df82a658766" + integrity sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA== + +"@esbuild/netbsd-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz#d633c09492a1721377f3bccedb2d821b911e813d" + integrity sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ== + +"@esbuild/openbsd-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz#17388c76e2f01125bf831a68c03a7ffccb65d1a2" + integrity sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw== + +"@esbuild/sunos-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz#e320636f00bb9f4fdf3a80e548cb743370d41767" + integrity sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ== + +"@esbuild/win32-arm64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz#c778b45a496e90b6fc373e2a2bb072f1441fe0ee" + integrity sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ== + +"@esbuild/win32-ia32@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz#481a65fee2e5cce74ec44823e6b09ecedcc5194c" + integrity sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg== + +"@esbuild/win32-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz#a5d300008960bb39677c46bf16f53ec70d8dee04" + integrity sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw== + "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -1202,6 +1317,13 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== + dependencies: + "@sinclair/typebox" "^0.27.8" + "@jridgewell/gen-mapping@^0.3.0": version "0.3.3" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" @@ -1235,6 +1357,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== +"@jridgewell/sourcemap-codec@^1.4.15": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + "@jridgewell/trace-mapping@^0.3.17": version "0.3.18" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" @@ -1327,6 +1454,76 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@rollup/rollup-android-arm-eabi@4.9.6": + version "4.9.6" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz#66b8d9cb2b3a474d115500f9ebaf43e2126fe496" + integrity sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg== + +"@rollup/rollup-android-arm64@4.9.6": + version "4.9.6" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz#46327d5b86420d2307946bec1535fdf00356e47d" + integrity sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw== + +"@rollup/rollup-darwin-arm64@4.9.6": + version "4.9.6" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz#166987224d2f8b1e2fd28ee90c447d52271d5e90" + integrity sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw== + +"@rollup/rollup-darwin-x64@4.9.6": + version "4.9.6" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz#a2e6e096f74ccea6e2f174454c26aef6bcdd1274" + integrity sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog== + +"@rollup/rollup-linux-arm-gnueabihf@4.9.6": + version "4.9.6" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz#09fcd4c55a2d6160c5865fec708a8e5287f30515" + integrity sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ== + +"@rollup/rollup-linux-arm64-gnu@4.9.6": + version "4.9.6" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz#19a3c0b6315c747ca9acf86e9b710cc2440f83c9" + integrity sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ== + +"@rollup/rollup-linux-arm64-musl@4.9.6": + version "4.9.6" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz#94aaf95fdaf2ad9335983a4552759f98e6b2e850" + integrity sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ== + +"@rollup/rollup-linux-riscv64-gnu@4.9.6": + version "4.9.6" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz#160510e63f4b12618af4013bddf1761cf9fc9880" + integrity sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA== + +"@rollup/rollup-linux-x64-gnu@4.9.6": + version "4.9.6" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz#5ac5d068ce0726bd0a96ca260d5bd93721c0cb98" + integrity sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw== + +"@rollup/rollup-linux-x64-musl@4.9.6": + version "4.9.6" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz#bafa759ab43e8eab9edf242a8259ffb4f2a57a5d" + integrity sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ== + +"@rollup/rollup-win32-arm64-msvc@4.9.6": + version "4.9.6" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz#1cc3416682e5a20d8f088f26657e6e47f8db468e" + integrity sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA== + +"@rollup/rollup-win32-ia32-msvc@4.9.6": + version "4.9.6" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz#7d2251e1aa5e8a1e47c86891fe4547a939503461" + integrity sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ== + +"@rollup/rollup-win32-x64-msvc@4.9.6": + version "4.9.6" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz#2c1fb69e02a3f1506f52698cfdc3a8b6386df9a6" + integrity sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ== + +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + "@supercharge/promise-pool@^2.3.2": version "2.3.2" resolved "https://registry.yarnpkg.com/@supercharge/promise-pool/-/promise-pool-2.3.2.tgz#6366894a7e7bc699bb65e58d8c828113729cf481" @@ -1339,6 +1536,11 @@ dependencies: tslib "^2.4.0" +"@types/estree@1.0.5", "@types/estree@^1.0.0": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + "@types/json-schema@^7.0.12": version "7.0.13" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.13.tgz#02c24f4363176d2d18fc8b70b9f3c54aba178a85" @@ -1474,11 +1676,65 @@ "@typescript-eslint/types" "6.7.5" eslint-visitor-keys "^3.4.1" +"@vitest/expect@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-1.2.1.tgz#574c0ac138a9e34522da202ea4c48a3adfe7240e" + integrity sha512-/bqGXcHfyKgFWYwIgFr1QYDaR9e64pRKxgBNWNXPefPFRhgm+K3+a/dS0cUGEreWngets3dlr8w8SBRw2fCfFQ== + dependencies: + "@vitest/spy" "1.2.1" + "@vitest/utils" "1.2.1" + chai "^4.3.10" + +"@vitest/runner@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-1.2.1.tgz#13e65b47eb04e572b99757e55f063f8f025822b2" + integrity sha512-zc2dP5LQpzNzbpaBt7OeYAvmIsRS1KpZQw4G3WM/yqSV1cQKNKwLGmnm79GyZZjMhQGlRcSFMImLjZaUQvNVZQ== + dependencies: + "@vitest/utils" "1.2.1" + p-limit "^5.0.0" + pathe "^1.1.1" + +"@vitest/snapshot@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-1.2.1.tgz#bd2dcae2322b90bab1660421ff9dae73fc84ecc0" + integrity sha512-Tmp/IcYEemKaqAYCS08sh0vORLJkMr0NRV76Gl8sHGxXT5151cITJCET20063wk0Yr/1koQ6dnmP6eEqezmd/Q== + dependencies: + magic-string "^0.30.5" + pathe "^1.1.1" + pretty-format "^29.7.0" + +"@vitest/spy@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-1.2.1.tgz#2777444890de9d32e55e600e34a13b2074cabc18" + integrity sha512-vG3a/b7INKH7L49Lbp0IWrG6sw9j4waWAucwnksPB1r1FTJgV7nkBByd9ufzu6VWya/QTvQW4V9FShZbZIB2UQ== + dependencies: + tinyspy "^2.2.0" + +"@vitest/utils@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-1.2.1.tgz#ad798cb13ec9e9e97b13be65d135e9e8e3c586aa" + integrity sha512-bsH6WVZYe/J2v3+81M5LDU8kW76xWObKIURpPrOXm2pjBniBu2MERI/XP60GpS4PHU3jyK50LUutOwrx4CyHUg== + dependencies: + diff-sequences "^29.6.3" + estree-walker "^3.0.3" + loupe "^2.3.7" + pretty-format "^29.7.0" + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== +acorn-walk@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa" + integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A== + +acorn@^8.10.0, acorn@^8.11.3: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== + acorn@^8.8.0: version "8.8.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" @@ -1513,6 +1769,11 @@ ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + anymatch@~3.1.2: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" @@ -1531,6 +1792,11 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +assertion-error@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" + integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== + babel-plugin-polyfill-corejs2@^0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.3.tgz#75044d90ba5043a5fb559ac98496f62f3eb668fd" @@ -1607,6 +1873,11 @@ busboy@1.6.0: dependencies: streamsearch "^1.1.0" +cac@^6.7.14: + version "6.7.14" + resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" + integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -1627,6 +1898,19 @@ caniuse-lite@^1.0.30001489: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001495.tgz#64a0ccef1911a9dcff647115b4430f8eff1ef2d9" integrity sha512-F6x5IEuigtUfU5ZMQK2jsy5JqUUlEFRVZq8bO2a+ysq5K7jD6PPc9YXZj78xDNS3uNchesp1Jw47YXEqr+Viyg== +chai@^4.3.10: + version "4.4.1" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.4.1.tgz#3603fa6eba35425b0f2ac91a009fe924106e50d1" + integrity sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g== + dependencies: + assertion-error "^1.1.0" + check-error "^1.0.3" + deep-eql "^4.1.3" + get-func-name "^2.0.2" + loupe "^2.3.6" + pathval "^1.1.1" + type-detect "^4.0.8" + chalk@^2.0.0: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -1644,6 +1928,13 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +check-error@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" + integrity sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg== + dependencies: + get-func-name "^2.0.2" + chokidar@^3.4.0: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" @@ -1717,7 +2008,7 @@ core-js-compat@^3.30.1, core-js-compat@^3.30.2: dependencies: browserslist "^4.21.5" -cross-spawn@^7.0.2: +cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -1738,11 +2029,23 @@ debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: dependencies: ms "2.1.2" +deep-eql@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.3.tgz#7c7775513092f7df98d8df9996dd085eb668cc6d" + integrity sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw== + dependencies: + type-detect "^4.0.0" + deep-is@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +diff-sequences@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" + integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -1767,6 +2070,35 @@ electron-to-chromium@^1.4.411: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.425.tgz#399df13091b836d28283a545c25c8e4d9da86da8" integrity sha512-wv1NufHxu11zfDbY4fglYQApMswleE9FL/DSeyOyauVXDZ+Kco96JK/tPfBUaDqfRarYp2WH2hJ/5UnVywp9Jg== +esbuild@^0.19.3: + version "0.19.11" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.11.tgz#4a02dca031e768b5556606e1b468fe72e3325d96" + integrity sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA== + optionalDependencies: + "@esbuild/aix-ppc64" "0.19.11" + "@esbuild/android-arm" "0.19.11" + "@esbuild/android-arm64" "0.19.11" + "@esbuild/android-x64" "0.19.11" + "@esbuild/darwin-arm64" "0.19.11" + "@esbuild/darwin-x64" "0.19.11" + "@esbuild/freebsd-arm64" "0.19.11" + "@esbuild/freebsd-x64" "0.19.11" + "@esbuild/linux-arm" "0.19.11" + "@esbuild/linux-arm64" "0.19.11" + "@esbuild/linux-ia32" "0.19.11" + "@esbuild/linux-loong64" "0.19.11" + "@esbuild/linux-mips64el" "0.19.11" + "@esbuild/linux-ppc64" "0.19.11" + "@esbuild/linux-riscv64" "0.19.11" + "@esbuild/linux-s390x" "0.19.11" + "@esbuild/linux-x64" "0.19.11" + "@esbuild/netbsd-x64" "0.19.11" + "@esbuild/openbsd-x64" "0.19.11" + "@esbuild/sunos-x64" "0.19.11" + "@esbuild/win32-arm64" "0.19.11" + "@esbuild/win32-ia32" "0.19.11" + "@esbuild/win32-x64" "0.19.11" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -1902,11 +2234,33 @@ estraverse@^5.1.0, estraverse@^5.2.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== +estree-walker@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +execa@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c" + integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^8.0.1" + human-signals "^5.0.0" + is-stream "^3.0.0" + merge-stream "^2.0.0" + npm-run-path "^5.1.0" + onetime "^6.0.0" + signal-exit "^4.1.0" + strip-final-newline "^3.0.0" + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -1995,6 +2349,11 @@ fsevents@~2.3.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== +fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -2005,6 +2364,16 @@ gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== +get-func-name@^2.0.1, get-func-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" + integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== + +get-stream@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" + integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== + glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -2092,6 +2461,11 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +human-signals@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" + integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== + ignore@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" @@ -2159,6 +2533,11 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" + integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -2206,6 +2585,11 @@ json5@^2.2.2: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsonc-parser@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.1.tgz#031904571ccf929d7670ee8c547545081cb37f1a" + integrity sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA== + levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -2214,6 +2598,14 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +local-pkg@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.5.0.tgz#093d25a346bae59a99f80e75f6e9d36d7e8c925c" + integrity sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg== + dependencies: + mlly "^1.4.2" + pkg-types "^1.0.3" + locate-path@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" @@ -2243,6 +2635,13 @@ loose-envify@^1.1.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" +loupe@^2.3.6, loupe@^2.3.7: + version "2.3.7" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" + integrity sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA== + dependencies: + get-func-name "^2.0.1" + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -2257,6 +2656,13 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +magic-string@^0.30.5: + version "0.30.5" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9" + integrity sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -2265,6 +2671,11 @@ make-dir@^2.1.0: pify "^4.0.1" semver "^5.6.0" +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" @@ -2278,6 +2689,11 @@ micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" +mimic-fn@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" + integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== + minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -2285,6 +2701,16 @@ minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +mlly@^1.2.0, mlly@^1.4.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.5.0.tgz#8428a4617d54cc083d3009030ac79739a0e5447a" + integrity sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ== + dependencies: + acorn "^8.11.3" + pathe "^1.1.2" + pkg-types "^1.0.3" + ufo "^1.3.2" + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -2295,6 +2721,11 @@ nanoid@^3.3.4: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -2339,6 +2770,13 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +npm-run-path@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.2.0.tgz#224cdd22c755560253dd71b83a1ef2f758b2e955" + integrity sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg== + dependencies: + path-key "^4.0.0" + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -2346,6 +2784,13 @@ once@^1.3.0: dependencies: wrappy "1" +onetime@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" + integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== + dependencies: + mimic-fn "^4.0.0" + optionator@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" @@ -2365,6 +2810,13 @@ p-limit@^3.0.2: dependencies: yocto-queue "^0.1.0" +p-limit@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-5.0.0.tgz#6946d5b7140b649b7a33a027d89b4c625b3a5985" + integrity sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ== + dependencies: + yocto-queue "^1.0.0" + p-locate@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" @@ -2394,6 +2846,11 @@ path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== +path-key@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" + integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== + path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" @@ -2404,6 +2861,16 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pathe@^1.1.0, pathe@^1.1.1, pathe@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" + integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== + +pathval@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" + integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== + picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -2419,6 +2886,15 @@ pify@^4.0.1: resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== +pkg-types@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.0.3.tgz#988b42ab19254c01614d13f4f65a2cfc7880f868" + integrity sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A== + dependencies: + jsonc-parser "^3.2.0" + mlly "^1.2.0" + pathe "^1.1.0" + postcss@8.4.14: version "8.4.14" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf" @@ -2428,6 +2904,15 @@ postcss@8.4.14: picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^8.4.32: + version "8.4.33" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.33.tgz#1378e859c9f69bf6f638b990a0212f43e2aaa742" + integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.0.2" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -2445,6 +2930,15 @@ prettier@^2.7.1: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64" integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g== +pretty-format@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" + integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== + dependencies: + "@jest/schemas" "^29.6.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + punycode@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -2455,18 +2949,23 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -react-dom@^18.2.0: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" - integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== +react-dom@18.3.0-canary-b2d637128-20240123: + version "18.3.0-canary-b2d637128-20240123" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.0-canary-b2d637128-20240123.tgz#5f31e063da4dc8f4696dd32bd2319c401d5cbc87" + integrity sha512-n2EiCdWzI3tt73VgiGiSGIbyOhmpVdD3dTmk5VEmoCHv197Zf+JGJkeHVFQzHQILdrmHYoCZJcXJh0rPZz5fQA== dependencies: loose-envify "^1.1.0" - scheduler "^0.23.0" + scheduler "0.24.0-canary-b2d637128-20240123" -react@^18.2.0: +react-is@^18.0.0: version "18.2.0" - resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" - integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + +react@18.3.0-canary-b2d637128-20240123: + version "18.3.0-canary-b2d637128-20240123" + resolved "https://registry.yarnpkg.com/react/-/react-18.3.0-canary-b2d637128-20240123.tgz#6c5df9ae8411e90f401a76992f2bfd813a825352" + integrity sha512-VHZlpXe57Mtc3CM3GOKazuUof3y7xsL+S8uoE+vQGbSlS4tU+e2gLXc15U+/akw/c4cRqYt8Nl1pqIFpE5cN5Q== dependencies: loose-envify "^1.1.0" @@ -2551,6 +3050,28 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" +rollup@^4.2.0: + version "4.9.6" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.9.6.tgz#4515facb0318ecca254a2ee1315e22e09efc50a0" + integrity sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg== + dependencies: + "@types/estree" "1.0.5" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.9.6" + "@rollup/rollup-android-arm64" "4.9.6" + "@rollup/rollup-darwin-arm64" "4.9.6" + "@rollup/rollup-darwin-x64" "4.9.6" + "@rollup/rollup-linux-arm-gnueabihf" "4.9.6" + "@rollup/rollup-linux-arm64-gnu" "4.9.6" + "@rollup/rollup-linux-arm64-musl" "4.9.6" + "@rollup/rollup-linux-riscv64-gnu" "4.9.6" + "@rollup/rollup-linux-x64-gnu" "4.9.6" + "@rollup/rollup-linux-x64-musl" "4.9.6" + "@rollup/rollup-win32-arm64-msvc" "4.9.6" + "@rollup/rollup-win32-ia32-msvc" "4.9.6" + "@rollup/rollup-win32-x64-msvc" "4.9.6" + fsevents "~2.3.2" + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -2563,10 +3084,10 @@ safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -scheduler@^0.23.0: - version "0.23.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" - integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== +scheduler@0.24.0-canary-b2d637128-20240123: + version "0.24.0-canary-b2d637128-20240123" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.24.0-canary-b2d637128-20240123.tgz#9de9fe5020e7fb76c52819999674a17fe16e87c2" + integrity sha512-4DC2OVnTC6JFRcjtx+SNspgR2Eoqlas8oZeWHMgBk3IjrEi7RTVP68iv8lav0w/08P85rJn6R8NKGxd6DYdaUg== dependencies: loose-envify "^1.1.0" @@ -2599,6 +3120,16 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +siginfo@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30" + integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== + +signal-exit@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + slash@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" @@ -2614,6 +3145,16 @@ source-map-js@^1.0.2: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +stackback@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" + integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== + +std-env@^3.5.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" + integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== + streamsearch@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" @@ -2626,11 +3167,23 @@ strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" +strip-final-newline@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" + integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== + strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +strip-literal@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-1.3.0.tgz#db3942c2ec1699e6836ad230090b84bb458e3a07" + integrity sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg== + dependencies: + acorn "^8.10.0" + styled-jsx@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.1.tgz#839a1c3aaacc4e735fed0781b8619ea5d0009d1f" @@ -2662,6 +3215,21 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== +tinybench@^2.5.1: + version "2.6.0" + resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.6.0.tgz#1423284ee22de07c91b3752c048d2764714b341b" + integrity sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA== + +tinypool@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.8.2.tgz#84013b03dc69dacb322563a475d4c0a9be00f82a" + integrity sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ== + +tinyspy@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-2.2.0.tgz#9dc04b072746520b432f77ea2c2d17933de5d6ce" + integrity sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg== + to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -2691,6 +3259,11 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" +type-detect@^4.0.0, type-detect@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + type-fest@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" @@ -2701,6 +3274,11 @@ typescript@^5.1.0: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== +ufo@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.3.2.tgz#c7d719d0628a1c80c006d2240e0d169f6e3c0496" + integrity sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" @@ -2747,6 +3325,55 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +vite-node@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-1.2.1.tgz#bca96ae91b2b1ee9a7aa73685908362d70ce26a8" + integrity sha512-fNzHmQUSOY+y30naohBvSW7pPn/xn3Ib/uqm+5wAJQJiqQsU0NBR78XdRJb04l4bOFKjpTWld0XAfkKlrDbySg== + dependencies: + cac "^6.7.14" + debug "^4.3.4" + pathe "^1.1.1" + picocolors "^1.0.0" + vite "^5.0.0" + +vite@^5.0.0: + version "5.0.12" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.12.tgz#8a2ffd4da36c132aec4adafe05d7adde38333c47" + integrity sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w== + dependencies: + esbuild "^0.19.3" + postcss "^8.4.32" + rollup "^4.2.0" + optionalDependencies: + fsevents "~2.3.3" + +vitest@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-1.2.1.tgz#9afb705826a2c6260a71b625d28b49117833dce6" + integrity sha512-TRph8N8rnSDa5M2wKWJCMnztCZS9cDcgVTQ6tsTFTG/odHJ4l5yNVqvbeDJYJRZ6is3uxaEpFs8LL6QM+YFSdA== + dependencies: + "@vitest/expect" "1.2.1" + "@vitest/runner" "1.2.1" + "@vitest/snapshot" "1.2.1" + "@vitest/spy" "1.2.1" + "@vitest/utils" "1.2.1" + acorn-walk "^8.3.2" + cac "^6.7.14" + chai "^4.3.10" + debug "^4.3.4" + execa "^8.0.1" + local-pkg "^0.5.0" + magic-string "^0.30.5" + pathe "^1.1.1" + picocolors "^1.0.0" + std-env "^3.5.0" + strip-literal "^1.3.0" + tinybench "^2.5.1" + tinypool "^0.8.1" + vite "^5.0.0" + vite-node "1.2.1" + why-is-node-running "^2.2.2" + watchpack@2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" @@ -2762,6 +3389,14 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +why-is-node-running@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.2.2.tgz#4185b2b4699117819e7154594271e7e344c9973e" + integrity sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA== + dependencies: + siginfo "^2.0.0" + stackback "0.0.2" + word-wrap@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" @@ -2787,6 +3422,11 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +yocto-queue@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" + integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== + zod@3.21.4: version "3.21.4" resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" From 5af37b69432b90b7ca33f3fe8320e8f12cc2a913 Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Tue, 23 Jan 2024 19:23:56 +0100 Subject: [PATCH 17/39] Bump Node.js engine --- .github/workflows/release-next.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-next.yml b/.github/workflows/release-next.yml index 10ce16e..bdf11c0 100644 --- a/.github/workflows/release-next.yml +++ b/.github/workflows/release-next.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: - node-version: '16.x' + node-version: '18.x' registry-url: 'https://registry.npmjs.org' - name: Set outputs id: vars diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 50cd62a..6b79cf9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: - node-version: '16.x' + node-version: '18.x' registry-url: 'https://registry.npmjs.org' - run: yarn install - run: yarn build From b2d5dc62910cbdd30fd078a872441513ec1a9410 Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Wed, 24 Jan 2024 13:22:27 +0100 Subject: [PATCH 18/39] Add more tests for loader functions --- package.json | 4 +- test/server/utils/dataLoader.test.ts | 135 +++++++++++++++++++++++++-- yarn.lock | 26 +++--- 3 files changed, 143 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index b373ede..3f1dbb8 100644 --- a/package.json +++ b/package.json @@ -50,8 +50,8 @@ "eslint-plugin-simple-import-sort": "^8.0.0", "next": "^13.4.0", "prettier": "^2.7.1", - "react": "18.3.0-canary-b2d637128-20240123", - "react-dom": "18.3.0-canary-b2d637128-20240123", + "react": "18.2.0", + "react-dom": "18.2.0", "typescript": "^5.1.0", "vitest": "^1.2.1" }, diff --git a/test/server/utils/dataLoader.test.ts b/test/server/utils/dataLoader.test.ts index 124def9..e23de78 100644 --- a/test/server/utils/dataLoader.test.ts +++ b/test/server/utils/dataLoader.test.ts @@ -1,10 +1,12 @@ -import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; +import { headers } from 'next/headers'; +import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest'; -import { loadDocumentProps } from '../../../src/server'; +import { loadDocumentProps, loadDocumentPropsCached, loadPreviewDocumentProps } from '../../../src/server'; +import { DataLoaderOptions } from '../../../src/types'; describe('loadDocumentProps', () => { describe('without options', () => { - test('should throw an error if NEOS_BASE_URL is not set', async () => { + it('should throw an error if NEOS_BASE_URL is not set', async () => { await expect(loadDocumentProps({ slug: 'foo' })).rejects.toThrowError( 'Missing NEOS_BASE_URL environment variable' ); @@ -15,7 +17,7 @@ describe('loadDocumentProps', () => { vi.stubEnv('NEOS_BASE_URL', 'http://neos:1234'); }); - test('should fetch from configured API', async () => { + it('should fetch from configured API', async () => { const fetch = vi.fn().mockResolvedValue(createOkayFetchResponse({ meta: { title: 'Foo' } })); vi.stubGlobal('fetch', fetch); @@ -37,13 +39,132 @@ describe('loadDocumentProps', () => { await expect(loadDocumentProps({ slug: 'foo' }, { optional: true })).resolves.toBeUndefined(); }); }); +}); + +describe('loadDocumentPropsCached', () => { + beforeEach(() => { + vi.stubEnv('NEOS_BASE_URL', 'http://neos:1234'); + }); + + test('should cache the result', async () => { + const fetch = vi.fn().mockResolvedValue(createOkayFetchResponse({ meta: { title: 'Foo' } })); + vi.stubGlobal('fetch', fetch); + + await expect(loadDocumentPropsCached('foo')).resolves.toStrictEqual({ + meta: { title: 'Foo' }, + }); + + await expect(loadDocumentPropsCached('foo')).resolves.toStrictEqual({ + meta: { title: 'Foo' }, + }); + + expect(fetch).toHaveBeenCalledTimes(1); + }); + + test('should cache the result again', async () => { + const fetch = vi.fn().mockResolvedValue(createOkayFetchResponse({ meta: { title: 'Foo' } })); + vi.stubGlobal('fetch', fetch); + + await expect(loadDocumentPropsCached('foo')).resolves.toStrictEqual({ + meta: { title: 'Foo' }, + }); + + await expect(loadDocumentPropsCached('foo')).resolves.toStrictEqual({ + meta: { title: 'Foo' }, + }); + + expect(fetch).toHaveBeenCalledTimes(1); + }); +}); - afterEach(() => { - vi.unstubAllEnvs(); - vi.unstubAllGlobals(); +describe('loadPreviewDocumentProps', () => { + describe('with NEOS_BASE_URL set', () => { + beforeEach(() => { + vi.stubEnv('NEOS_BASE_URL', 'http://neos:1234'); + }); + + // TODO Add test without context path query param + // TODO Add test for PUBLIC_BASE_URL env and proxy headers + + describe('with Neos session cookie', () => { + beforeEach(() => { + vi.mocked(headers).mockReturnValue(new Headers({ Cookie: 'a-session-cookie' })); + }); + + describe.each<{ opts?: DataLoaderOptions }>([ + // No options + {}, + // Override cache, should have no effect for preview + { opts: { cache: 'default' } }, + ])('with options $opts', ({ opts }) => { + it('should fetch from configured API', async () => { + const fetch = vi.fn().mockResolvedValue(createOkayFetchResponse({ meta: { title: 'Foo Preview' } })); + vi.stubGlobal('fetch', fetch); + + await expect( + loadPreviewDocumentProps({ 'node[__contextNodePath]': 'foo/bar@user-me' }, opts) + ).resolves.toStrictEqual({ + meta: { title: 'Foo Preview' }, + }); + + expect(fetch).toHaveBeenCalledWith( + 'http://neos:1234/neos/content-api/document?contextPath=foo%2Fbar%40user-me', + { + cache: 'no-store', + headers: { + Cookie: 'a-session-cookie', + }, + next: undefined, + } + ); + }); + }); + }); }); }); +let reactCached = false; + +// Mock the `cache` export from React: +// 1. it is not exported in the _normal_ react version (using the canary channel would work though), but inside Next.js we can use it. +// 2. we don't want to use the real cache, because we can not control it in tests. +vi.mock('react', async (importOriginal) => { + const mod = await importOriginal(); + return { + ...mod, + // The cache function is a bit tricky, since it is used at the module level when importing the module, + // so we have to specify the implementation here and now - but still enable a reset of cached values in tests. + // eslint-disable-next-line @typescript-eslint/ban-types + cache: vi.fn((fn: CachedFunction): CachedFunction => { + // Cache the first call and return the cached value for all subsequent calls. + let result: any; + return ((...args: any[]) => { + if (reactCached) { + return result; + } + reactCached = true; + return (result = fn(...args)); + }) as any; + }), + }; +}); + +vi.mock('next/headers', async (importOriginal) => { + const mod = await importOriginal(); + return { + ...mod, + headers: vi.fn(), + }; +}); + +afterEach(() => { + vi.unstubAllEnvs(); + vi.unstubAllGlobals(); + vi.resetAllMocks(); + // Make sure React cache is reset after each test + reactCached = false; +}); + function createOkayFetchResponse(data: any) { return { ok: true, status: 200, json: () => new Promise((resolve) => resolve(data)) }; } diff --git a/yarn.lock b/yarn.lock index 7644842..4aac622 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2949,23 +2949,23 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -react-dom@18.3.0-canary-b2d637128-20240123: - version "18.3.0-canary-b2d637128-20240123" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.0-canary-b2d637128-20240123.tgz#5f31e063da4dc8f4696dd32bd2319c401d5cbc87" - integrity sha512-n2EiCdWzI3tt73VgiGiSGIbyOhmpVdD3dTmk5VEmoCHv197Zf+JGJkeHVFQzHQILdrmHYoCZJcXJh0rPZz5fQA== +react-dom@18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== dependencies: loose-envify "^1.1.0" - scheduler "0.24.0-canary-b2d637128-20240123" + scheduler "^0.23.0" react-is@^18.0.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== -react@18.3.0-canary-b2d637128-20240123: - version "18.3.0-canary-b2d637128-20240123" - resolved "https://registry.yarnpkg.com/react/-/react-18.3.0-canary-b2d637128-20240123.tgz#6c5df9ae8411e90f401a76992f2bfd813a825352" - integrity sha512-VHZlpXe57Mtc3CM3GOKazuUof3y7xsL+S8uoE+vQGbSlS4tU+e2gLXc15U+/akw/c4cRqYt8Nl1pqIFpE5cN5Q== +react@18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== dependencies: loose-envify "^1.1.0" @@ -3084,10 +3084,10 @@ safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -scheduler@0.24.0-canary-b2d637128-20240123: - version "0.24.0-canary-b2d637128-20240123" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.24.0-canary-b2d637128-20240123.tgz#9de9fe5020e7fb76c52819999674a17fe16e87c2" - integrity sha512-4DC2OVnTC6JFRcjtx+SNspgR2Eoqlas8oZeWHMgBk3IjrEi7RTVP68iv8lav0w/08P85rJn6R8NKGxd6DYdaUg== +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== dependencies: loose-envify "^1.1.0" From 466e16a8316b824decb379800c70a3ee862587fc Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Wed, 24 Jan 2024 13:25:30 +0100 Subject: [PATCH 19/39] Automatically run tests --- .github/workflows/test.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..3deb36e --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,17 @@ +name: Test +on: + push: + branches: + - main + pull_request: +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: '18.x' + cache: yarn + - run: yarn install + - run: yarn test From 0d12c0adb2b228b050c1696ab59a97330f7b8517 Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Wed, 24 Jan 2024 13:26:19 +0100 Subject: [PATCH 20/39] Update actions --- .github/workflows/release-next.yml | 6 +++--- .github/workflows/release.yml | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release-next.yml b/.github/workflows/release-next.yml index bdf11c0..e172a63 100644 --- a/.github/workflows/release-next.yml +++ b/.github/workflows/release-next.yml @@ -7,11 +7,11 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 with: node-version: '18.x' - registry-url: 'https://registry.npmjs.org' + cache: yarn - name: Set outputs id: vars run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6b79cf9..1740700 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,13 +6,13 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: '18.x' - registry-url: 'https://registry.npmjs.org' - - run: yarn install - - run: yarn build - - run: npm publish - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: '18.x' + cache: yarn + - run: yarn install + - run: yarn build + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} From a374262c29e4d10cd395f27e31670e33b131c60d Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Wed, 24 Jan 2024 13:45:10 +0100 Subject: [PATCH 21/39] Fix repository url, include test on next branch --- .github/workflows/release-next.yml | 1 + .github/workflows/release.yml | 1 + .github/workflows/test.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/workflows/release-next.yml b/.github/workflows/release-next.yml index e172a63..c2af59d 100644 --- a/.github/workflows/release-next.yml +++ b/.github/workflows/release-next.yml @@ -12,6 +12,7 @@ jobs: with: node-version: '18.x' cache: yarn + registry-url: 'https://registry.npmjs.org' - name: Set outputs id: vars run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1740700..7778a53 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,6 +11,7 @@ jobs: with: node-version: '18.x' cache: yarn + registry-url: 'https://registry.npmjs.org' - run: yarn install - run: yarn build - run: npm publish diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3deb36e..e153ca2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,6 +3,7 @@ on: push: branches: - main + - next pull_request: jobs: release: From 5ccd8e33907be2f853041e42885a4da9ab4ec8d5 Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Wed, 24 Jan 2024 15:05:00 +0100 Subject: [PATCH 22/39] Rename job --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e153ca2..e3433ec 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,7 +6,7 @@ on: - next pull_request: jobs: - release: + test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From 8369a44841290af1dc5b6b3b664e44a89c630d04 Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Wed, 24 Jan 2024 15:27:44 +0100 Subject: [PATCH 23/39] Add custom type for site data with extended properties --- src/server/utils/dataLoader.ts | 4 +- test/server/utils/dataLoader.test.ts | 84 ++++++++++++++++++++++------ 2 files changed, 69 insertions(+), 19 deletions(-) diff --git a/src/server/utils/dataLoader.ts b/src/server/utils/dataLoader.ts index 823d76d..f42b164 100644 --- a/src/server/utils/dataLoader.ts +++ b/src/server/utils/dataLoader.ts @@ -131,7 +131,7 @@ export const buildNeosHeaders = () => { return headers; }; -export const loadSiteProps = async (opts?: DataLoaderOptions) => { +export const loadSiteProps = async (opts?: DataLoaderOptions) => { const apiUrl = process.env.NEOS_BASE_URL; if (!apiUrl && opts?.optional) { return undefined; @@ -160,7 +160,7 @@ export const loadSiteProps = async (opts?: DataLoaderOptions) => { } } - const data: SiteData = await response.json(); + const data: CustomSiteData = await response.json(); const endTime = Date.now(); log.debug('fetched data from content API with url', fetchUrl, ', took', `${endTime - startTime}ms`); diff --git a/test/server/utils/dataLoader.test.ts b/test/server/utils/dataLoader.test.ts index e23de78..35d41cc 100644 --- a/test/server/utils/dataLoader.test.ts +++ b/test/server/utils/dataLoader.test.ts @@ -1,35 +1,47 @@ import { headers } from 'next/headers'; import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest'; -import { loadDocumentProps, loadDocumentPropsCached, loadPreviewDocumentProps } from '../../../src/server'; +import { + loadDocumentProps, + loadDocumentPropsCached, + loadPreviewDocumentProps, + loadSiteProps, +} from '../../../src/server'; import { DataLoaderOptions } from '../../../src/types'; describe('loadDocumentProps', () => { - describe('without options', () => { - it('should throw an error if NEOS_BASE_URL is not set', async () => { - await expect(loadDocumentProps({ slug: 'foo' })).rejects.toThrowError( - 'Missing NEOS_BASE_URL environment variable' - ); - }); + it('should throw an error if NEOS_BASE_URL is not set', async () => { + await expect(loadDocumentProps({ slug: 'foo' })).rejects.toThrowError('Missing NEOS_BASE_URL environment variable'); + }); - describe('with NEOS_BASE_URL set', () => { - beforeEach(() => { - vi.stubEnv('NEOS_BASE_URL', 'http://neos:1234'); - }); + describe('with NEOS_BASE_URL set', () => { + beforeEach(() => { + vi.stubEnv('NEOS_BASE_URL', 'http://neos:1234'); + }); + describe.each<{ opts?: DataLoaderOptions; expectedFetchConfig: RequestInit }>([ + // No options + { expectedFetchConfig: { cache: 'no-store', headers: {}, next: undefined } }, + // Specify cache + { opts: { cache: 'default' }, expectedFetchConfig: { cache: 'default', headers: {}, next: undefined } }, + // Specify cache and next tags + { + opts: { cache: 'force-cache', next: { tags: ['neos'] } }, + expectedFetchConfig: { cache: 'force-cache', headers: {}, next: { tags: ['neos'] } }, + }, + ])('with options $opts', ({ opts, expectedFetchConfig }) => { it('should fetch from configured API', async () => { const fetch = vi.fn().mockResolvedValue(createOkayFetchResponse({ meta: { title: 'Foo' } })); vi.stubGlobal('fetch', fetch); - await expect(loadDocumentProps({ slug: 'foo' })).resolves.toStrictEqual({ + await expect(loadDocumentProps({ slug: 'foo' }, opts)).resolves.toStrictEqual({ meta: { title: 'Foo' }, }); - expect(fetch).toHaveBeenCalledWith('http://neos:1234/neos/content-api/document?path=%2Ffoo', { - cache: 'no-store', - headers: {}, - next: undefined, - }); + expect(fetch).toHaveBeenCalledWith( + 'http://neos:1234/neos/content-api/document?path=%2Ffoo', + expectedFetchConfig + ); }); }); }); @@ -123,6 +135,44 @@ describe('loadPreviewDocumentProps', () => { }); }); +describe('loadSiteProps', () => { + it('should throw an error if NEOS_BASE_URL is not set', async () => { + await expect(loadSiteProps()).rejects.toThrowError('Missing NEOS_BASE_URL environment variable'); + }); + + describe('with NEOS_BASE_URL set', () => { + beforeEach(() => { + vi.stubEnv('NEOS_BASE_URL', 'http://neos:1234'); + }); + + describe.each<{ opts?: DataLoaderOptions; expectedFetchConfig: RequestInit }>([ + // No options + { expectedFetchConfig: { cache: 'no-store', headers: {}, next: undefined } }, + // Override cache + { opts: { cache: 'default' }, expectedFetchConfig: { cache: 'default', headers: {}, next: undefined } }, + ])('with options $opts', ({ opts, expectedFetchConfig }) => { + it('should fetch from configured API', async () => { + const fetch = vi.fn().mockResolvedValue(createOkayFetchResponse({ meta: { title: 'Foo' } })); + vi.stubGlobal('fetch', fetch); + + await expect(loadSiteProps(opts)).resolves.toStrictEqual({ + meta: { title: 'Foo' }, + }); + + expect(fetch).toHaveBeenCalledWith('http://neos:1234/neos/content-api/site', expectedFetchConfig); + }); + }); + }); + + describe('with optional option', () => { + test('should not throw an error if NEOS_BASE_URL is not set', async () => { + await expect(loadSiteProps({ optional: true })).resolves.toBeUndefined(); + }); + }); +}); + +// --- Mock implementations --- + let reactCached = false; // Mock the `cache` export from React: From 1cfc956d0914ffdf3305c46567b489427ee3664e Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Wed, 31 Jan 2024 13:03:08 +0100 Subject: [PATCH 24/39] Fix import paths --- src/server/components/ContentCollection.tsx | 3 +-- src/server/components/ContentCollectionProvider.tsx | 3 +-- src/server/components/ContentComponent.tsx | 3 +-- src/server/components/ContentComponentProvider.tsx | 3 +-- src/server/components/Editable.tsx | 3 +-- src/server/utils/hooks.ts | 3 +-- 6 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/server/components/ContentCollection.tsx b/src/server/components/ContentCollection.tsx index 0a33e61..ec49423 100644 --- a/src/server/components/ContentCollection.tsx +++ b/src/server/components/ContentCollection.tsx @@ -1,5 +1,4 @@ -import { ContextProps } from 'src/types'; - +import { ContextProps } from '../../types'; import { useInBackend } from '../utils/hooks'; import ContentCollectionProvider from './ContentCollectionProvider'; diff --git a/src/server/components/ContentCollectionProvider.tsx b/src/server/components/ContentCollectionProvider.tsx index 788f693..ebb0485 100644 --- a/src/server/components/ContentCollectionProvider.tsx +++ b/src/server/components/ContentCollectionProvider.tsx @@ -1,5 +1,4 @@ -import { ContextProps } from 'src/types'; - +import { ContextProps } from '../../types'; import { useInBackend } from '../utils/hooks'; import { useContentCollection } from '../utils/hooks'; import ContentComponentIncludes from './client/ContentComponentIncludes'; diff --git a/src/server/components/ContentComponent.tsx b/src/server/components/ContentComponent.tsx index ed9ab85..f13cd80 100644 --- a/src/server/components/ContentComponent.tsx +++ b/src/server/components/ContentComponent.tsx @@ -1,5 +1,4 @@ -import { ContextProps } from 'src/types'; - +import { ContextProps } from '../../types'; import ContentComponentProvider from './ContentComponentProvider'; type ContentComponentProps = { diff --git a/src/server/components/ContentComponentProvider.tsx b/src/server/components/ContentComponentProvider.tsx index f559c6e..0765a55 100644 --- a/src/server/components/ContentComponentProvider.tsx +++ b/src/server/components/ContentComponentProvider.tsx @@ -1,5 +1,4 @@ -import { ContextProps } from 'src/types'; - +import { ContextProps } from '../../types'; import { useInBackend } from '../utils/hooks'; import { useContentComponent } from '../utils/hooks'; import ContentComponentIncludes from './client/ContentComponentIncludes'; diff --git a/src/server/components/Editable.tsx b/src/server/components/Editable.tsx index 066adfa..57f296e 100644 --- a/src/server/components/Editable.tsx +++ b/src/server/components/Editable.tsx @@ -1,5 +1,4 @@ -import { ContextProps } from 'src/types'; - +import { ContextProps } from '../../types'; import { useEditPreviewMode, useInBackend, useNode } from '../utils/hooks'; type EditableProps = { diff --git a/src/server/utils/hooks.ts b/src/server/utils/hooks.ts index 7bc981a..be97527 100644 --- a/src/server/utils/hooks.ts +++ b/src/server/utils/hooks.ts @@ -1,5 +1,4 @@ -import { ContextProps, DataLoaderOptions } from 'src/types'; - +import { ContextProps, DataLoaderOptions } from '../../types'; import { loadDocumentPropsCached, loadPreviewDocumentPropsCached } from './dataLoader'; import { resolveCurrentNode } from './helper'; From 07515392c0177778043eaa7c22cc89bcf20f57f5 Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Wed, 31 Jan 2024 14:32:05 +0100 Subject: [PATCH 25/39] Deprecate async hooks and provide with* replacements The default ESLint rule for React hooks errors on calling functions like `useNode` from async functions (server components). It should not be necessary to configure ESLint rules to use Zebra, so we rather change the names of our functions now. Because they are not _really_ hooks I settled for the name `with*` to differentiate from the actual data loaders. --- src/server.ts | 7 ++ src/server/utils/dataLoader.ts | 45 +++++--- src/server/utils/hooks.ts | 198 +++++++++++++++++++-------------- src/types/index.ts | 12 +- 4 files changed, 154 insertions(+), 108 deletions(-) diff --git a/src/server.ts b/src/server.ts index dcca482..04edb70 100644 --- a/src/server.ts +++ b/src/server.ts @@ -21,5 +21,12 @@ export { useMeta, useNode, useSiteNode, + withContentCollection, + withContentComponent, + withDocumentNode, + withEditPreviewMode, + withMeta, + withNode, + withSiteNode, } from './server/utils/hooks'; export { getNodeType, initNodeTypes } from './server/utils/nodeTypes'; diff --git a/src/server/utils/dataLoader.ts b/src/server/utils/dataLoader.ts index f42b164..af60982 100644 --- a/src/server/utils/dataLoader.ts +++ b/src/server/utils/dataLoader.ts @@ -2,11 +2,14 @@ import log from 'loglevel'; import { headers as nextHeaders } from 'next/headers'; import { cache } from 'react'; -import { ApiErrors, DataLoaderOptions, NeosData, SiteData } from '../../types'; +import { ApiErrors, DataLoaderOptions, NeosData, OptionalOption, SiteData } from '../../types'; log.setDefaultLevel(log.levels.DEBUG); -export const loadDocumentProps = async (params: { slug: string | string[] }, opts?: DataLoaderOptions) => { +export const loadDocumentProps = async ( + params: { slug: string | string[] }, + opts?: DataLoaderOptions & OptionalOption +) => { const apiUrl = process.env.NEOS_BASE_URL; if (!apiUrl && opts?.optional) { return undefined; @@ -58,7 +61,7 @@ export const loadDocumentProps = async (params: { slug: string | string[] }, opt export const loadPreviewDocumentProps = async ( searchParams: { [key: string]: string | string[] | undefined }, - opts?: DataLoaderOptions + opts?: DataLoaderOptions & OptionalOption ) => { const apiUrl = process.env.NEOS_BASE_URL; if (!apiUrl && opts?.optional) { @@ -102,23 +105,27 @@ export const loadPreviewDocumentProps = async ( return data; }; -export const loadDocumentPropsCached = cache((routePath: string | undefined, opts?: DataLoaderOptions) => { - if (!routePath) { - return undefined; +export const loadDocumentPropsCached = cache( + (routePath: string | undefined, opts?: DataLoaderOptions & OptionalOption) => { + if (!routePath) { + return undefined; + } + log.debug('fetching data from Neos inside cache with route path', routePath); + const slug = routePath.split('/').filter((s) => s.length > 0); + return loadDocumentProps({ slug }, opts); } - log.debug('fetching data from Neos inside cache with route path', routePath); - const slug = routePath.split('/').filter((s) => s.length > 0); - return loadDocumentProps({ slug }, opts); -}); +); -export const loadPreviewDocumentPropsCached = cache((contextNodePath: string | undefined, opts?: DataLoaderOptions) => { - if (!contextNodePath) { - return undefined; +export const loadPreviewDocumentPropsCached = cache( + (contextNodePath: string | undefined, opts?: DataLoaderOptions & OptionalOption) => { + if (!contextNodePath) { + return undefined; + } + log.debug('fetching data from Neos inside cache with context node path', contextNodePath); + const searchParams = { 'node[__contextNodePath]': contextNodePath }; + return loadPreviewDocumentProps(searchParams, opts); } - log.debug('fetching data from Neos inside cache with context node path', contextNodePath); - const searchParams = { 'node[__contextNodePath]': contextNodePath }; - return loadPreviewDocumentProps(searchParams, opts); -}); +); export const buildNeosHeaders = () => { const headers: Record = {}; @@ -131,7 +138,9 @@ export const buildNeosHeaders = () => { return headers; }; -export const loadSiteProps = async (opts?: DataLoaderOptions) => { +export const loadSiteProps = async ( + opts?: DataLoaderOptions & OptionalOption +) => { const apiUrl = process.env.NEOS_BASE_URL; if (!apiUrl && opts?.optional) { return undefined; diff --git a/src/server/utils/hooks.ts b/src/server/utils/hooks.ts index be97527..9f40c9d 100644 --- a/src/server/utils/hooks.ts +++ b/src/server/utils/hooks.ts @@ -1,123 +1,151 @@ -import { ContextProps, DataLoaderOptions } from '../../types'; +import { ContextProps, DataLoaderOptions, OptionalOption } from '../../types'; import { loadDocumentPropsCached, loadPreviewDocumentPropsCached } from './dataLoader'; import { resolveCurrentNode } from './helper'; -export const useMeta = (ctx: ContextProps, opts?: DataLoaderOptions) => { - return async () => { - const neosData = ctx.inBackend - ? await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts) - : await loadDocumentPropsCached(ctx.routePath, opts); +/** + * @deprecated Use withMeta instead (async hooks are not allowed by ESLint rules) + */ +export const useMeta = (ctx: ContextProps, opts?: DataLoaderOptions & OptionalOption) => { + return () => withMeta(ctx, opts); +}; - return neosData?.meta; - }; +export const withMeta = async (ctx: ContextProps, opts?: DataLoaderOptions) => { + const neosData = ctx.inBackend + ? await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts) + : await loadDocumentPropsCached(ctx.routePath, opts); + if (!neosData) { + throw new Error(`Node not found: ${ctx.inBackend ? ctx.contextNodePath : ctx.routePath}`); + } + + return neosData.meta; }; -export const useNode = (ctx: ContextProps, opts?: DataLoaderOptions) => { - return async () => { - const neosData = ctx.inBackend - ? await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts) - : await loadDocumentPropsCached(ctx.routePath, opts); +/** + * @deprecated Use withNode instead (async hooks are not allowed by ESLint rules) + */ +export const useNode = (ctx: ContextProps, opts?: DataLoaderOptions & OptionalOption) => { + return () => withNode(ctx, opts); +}; - if (!neosData) { - return undefined; - } +export const withNode = async (ctx: ContextProps, opts?: DataLoaderOptions) => { + const neosData = ctx.inBackend + ? await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts) + : await loadDocumentPropsCached(ctx.routePath, opts); + if (!neosData) { + throw new Error(`Document node not found: ${ctx.inBackend ? ctx.contextNodePath : ctx.routePath}`); + } - const node = resolveCurrentNode(ctx, neosData); + const node = resolveCurrentNode(ctx, neosData); + if (!node) { + throw new Error(`Could not resolve current node: ${ctx.currentNodeIdentifier}`); + } - return node; - }; + return node; }; +/** + * @deprecated Use withDocumentNode instead (async hooks are not allowed by ESLint rules) + */ export const useDocumentNode = (ctx: ContextProps, opts?: DataLoaderOptions) => { - return async () => { - const neosData = ctx.inBackend - ? await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts) - : await loadDocumentPropsCached(ctx.routePath, opts); + return () => withDocumentNode(ctx, opts); +}; - return neosData?.node; - }; +export const withDocumentNode = async (ctx: ContextProps, opts?: DataLoaderOptions) => { + const neosData = ctx.inBackend + ? await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts) + : await loadDocumentPropsCached(ctx.routePath, opts); + if (!neosData) { + throw new Error(`Document node not found: ${ctx.inBackend ? ctx.contextNodePath : ctx.routePath}`); + } + + return neosData.node; }; +/** + * @deprecated Use withSiteNode instead (async hooks are not allowed by ESLint rules) + */ export const useSiteNode = (ctx: ContextProps, opts?: DataLoaderOptions) => { - return async () => { - const neosData = ctx.inBackend - ? await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts) - : await loadDocumentPropsCached(ctx.routePath, opts); + return () => withSiteNode(ctx, opts); +}; - return neosData?.site; - }; +export const withSiteNode = async (ctx: ContextProps, opts?: DataLoaderOptions) => { + const neosData = ctx.inBackend + ? await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts) + : await loadDocumentPropsCached(ctx.routePath, opts); + if (!neosData) { + throw new Error(`Document node not found: ${ctx.inBackend ? ctx.contextNodePath : ctx.routePath}`); + } + + return neosData.site; }; export const useInBackend = (ctx: ContextProps) => { return !!ctx?.inBackend; }; +/** + * @deprecated Use withEditPreviewMode instead (async hooks are not allowed by ESLint rules) + */ export const useEditPreviewMode = (ctx: ContextProps, opts?: DataLoaderOptions) => { - return async () => { - const neosData = await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts); + return () => withEditPreviewMode(ctx, opts); +}; - return neosData?.backend?.editPreviewMode; - }; +export const withEditPreviewMode = async (ctx: ContextProps, opts?: DataLoaderOptions) => { + const neosData = await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts); + if (!neosData) { + throw new Error(`Document node not found: ${ctx.contextNodePath}`); + } + + return neosData.backend?.editPreviewMode; }; +/** + * @deprecated Use withContentCollection instead (async hooks are not allowed by ESLint rules) + */ export const useContentCollection = (ctx: ContextProps, nodeName?: string, opts?: DataLoaderOptions) => { + return () => withContentCollection(ctx, nodeName, opts); +}; + +export const withContentCollection = async (ctx: ContextProps, nodeName?: string, opts?: DataLoaderOptions) => { const inBackend = useInBackend(ctx); - return async () => { - const neosData = inBackend - ? await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts) - : await loadDocumentPropsCached(ctx.routePath, opts); - - if (!neosData) { - return { - collectionNode: undefined, - collectionProps: {}, - }; - } - - const currentNode = resolveCurrentNode(ctx, neosData); - const collectionNode = nodeName ? currentNode?.children?.find((child) => child.nodeName === nodeName) : currentNode; - - if (!collectionNode) { - return { - collectionNode: undefined, - collectionProps: {}, - }; - } - - return { - collectionNode, - collectionProps: { - 'data-__neos-node-contextpath': inBackend ? collectionNode.contextPath : undefined, - // Use a fixed fusion path to render an out-of-band preview of this node - 'data-__neos-fusion-path': inBackend ? 'neosNext/previewNode' : undefined, - 'data-__neos-insertion-anchor': inBackend ? true : undefined, - }, - }; + const currentNode = await withNode(ctx, opts); + + const collectionNode = nodeName ? currentNode.children?.find((child) => child.nodeName === nodeName) : currentNode; + if (!collectionNode) { + throw new Error(`Child node not found: ${nodeName}`); + } + + return { + collectionNode, + collectionProps: { + 'data-__neos-node-contextpath': inBackend ? collectionNode.contextPath : undefined, + // Use a fixed fusion path to render an out-of-band preview of this node + 'data-__neos-fusion-path': inBackend ? 'neosNext/previewNode' : undefined, + 'data-__neos-insertion-anchor': inBackend ? true : undefined, + }, }; }; +/** + * @deprecated Use withContentComponent instead (async hooks are not allowed by ESLint rules) + */ export const useContentComponent = (ctx: ContextProps, opts?: DataLoaderOptions) => { + return () => withContentComponent(ctx, opts); +}; + +export const withContentComponent = async (ctx: ContextProps, opts?: DataLoaderOptions) => { const inBackend = useInBackend(ctx); - return async () => { - const node = await useNode(ctx, opts)(); - - if (!node) { - return { - componentNode: undefined, - componentProps: {}, - }; - } - - return { - componentNode: node, - componentProps: { - 'data-__neos-node-contextpath': inBackend ? node.contextPath : undefined, - // Use a fixed fusion path to render an out-of-band preview of this node. - // The Networkteam.Neos.Next package provides a Fusion prototype that renders the node through Next.js. - 'data-__neos-fusion-path': inBackend ? 'neosNext/previewNode' : undefined, - }, - }; + const node = await withNode(ctx, opts); + + return { + componentNode: node, + componentProps: { + 'data-__neos-node-contextpath': inBackend ? node.contextPath : undefined, + // Use a fixed fusion path to render an out-of-band preview of this node. + // The Networkteam.Neos.Next package provides a Fusion prototype that renders the node through Next.js. + 'data-__neos-fusion-path': inBackend ? 'neosNext/previewNode' : undefined, + }, }; }; diff --git a/src/types/index.ts b/src/types/index.ts index f495fc9..dc6fb17 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -123,11 +123,6 @@ export type ContextProps = { }; export type DataLoaderOptions = { - /** - * If true, the data loader will not throw an error if the content API base URL is not set and no data can be fetched. - */ - optional?: boolean; - /** * The fetch request cache mode to use. */ @@ -138,3 +133,10 @@ export type DataLoaderOptions = { */ next?: NextFetchRequestConfig; }; + +export type OptionalOption = { + /** + * If true, the data loader will not throw an error if the content API base URL is not set and no data can be fetched. + */ + optional?: boolean; +}; From 9ac7e0f4816bb0f0fecc3d2a43f522f5e4a0b05f Mon Sep 17 00:00:00 2001 From: Rasmus Schmidt Date: Fri, 2 Feb 2024 17:24:19 +0100 Subject: [PATCH 26/39] slightly update readme examples --- README.md | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index e095f32..d071c95 100644 --- a/README.md +++ b/README.md @@ -100,27 +100,24 @@ Component for a basic document page: ```tsx import { ContentCollection, - ContentComponent, - NeosContentNode, - NeosContext, - useMeta, - useSiteNode, -} from '@networkteam/zebra'; -import { useContext } from 'react'; + withMeta, +} from '@networkteam/zebra/server'; import Header from './partials/Header'; -const DocumentPage = () => { - const meta = useMeta(); +const DocumentPage = async ({ ctx }: { ctx: ContextProps }) => { + const meta = withMeta(ctx); + const inBackend = ctx.inBackend; return (
- +
); @@ -167,16 +164,20 @@ export default Headline; Integrational component for a headline: ```tsx -import { ContentComponent, Editable, useNode } from '@networkteam/zebra'; +import { ContextProps } from '@networkteam/zebra'; +import { ContentComponent, Editable, withNode } from '@networkteam/zebra/server'; + +import { baseClasses } from '@/lib/utils/baseClasses'; import Headline from '../ui/Headline'; -const ContentHeadline = () => { - const node = useNode(); +const ContentHeadline = async ({ ctx }: { ctx: ContextProps }) => { + const node = await withNode(ctx); + return ( - - - + + + ); From 65588cae289e7fe80a3d3adf826cebcb4d980047 Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Wed, 7 Feb 2024 18:16:56 +0100 Subject: [PATCH 27/39] Add custom error type to better inspect errors if no JSON is returned from content API --- src/server.ts | 1 + src/server/utils/ApiError.ts | 18 ++++++++++ src/server/utils/dataLoader.ts | 34 ++++++++++++------- src/server/utils/hooks.ts | 3 ++ .../__snapshots__/dataLoader.test.ts.snap | 5 +++ test/server/utils/dataLoader.test.ts | 31 +++++++++++++++++ 6 files changed, 80 insertions(+), 12 deletions(-) create mode 100644 src/server/utils/ApiError.ts create mode 100644 test/server/utils/__snapshots__/dataLoader.test.ts.snap diff --git a/src/server.ts b/src/server.ts index 04edb70..80ed74c 100644 --- a/src/server.ts +++ b/src/server.ts @@ -4,6 +4,7 @@ export { default as ContentComponent } from './server/components/ContentComponen export { default as ContentComponentProvider } from './server/components/ContentComponentProvider'; export { default as Editable } from './server/components/Editable'; export { default as NodeRenderer } from './server/components/NodeRenderer'; +export { default as ApiError } from './server/utils/ApiError'; export { loadDocumentProps, loadDocumentPropsCached, diff --git a/src/server/utils/ApiError.ts b/src/server/utils/ApiError.ts new file mode 100644 index 0000000..8bb11a1 --- /dev/null +++ b/src/server/utils/ApiError.ts @@ -0,0 +1,18 @@ +import { ApiErrors } from 'src/types'; + +export default class ApiError extends Error { + constructor( + public readonly errorMessage: string, + public readonly status: number, + public readonly url: string, + public readonly errors?: ApiErrors['errors'], + public readonly responseBody?: string + ) { + super(`${errorMessage}, status: ${status}, url: ${url}` + (errors ? `, errors: ${errors.join(',')}` : '')); + this.errorMessage = errorMessage; + this.status = status; + this.url = url; + this.errors = errors; + this.responseBody = responseBody; + } +} diff --git a/src/server/utils/dataLoader.ts b/src/server/utils/dataLoader.ts index af60982..444f70d 100644 --- a/src/server/utils/dataLoader.ts +++ b/src/server/utils/dataLoader.ts @@ -3,6 +3,7 @@ import { headers as nextHeaders } from 'next/headers'; import { cache } from 'react'; import { ApiErrors, DataLoaderOptions, NeosData, OptionalOption, SiteData } from '../../types'; +import ApiError from './ApiError'; log.setDefaultLevel(log.levels.DEBUG); @@ -44,12 +45,7 @@ export const loadDocumentProps = async ( return undefined; } - const data: ApiErrors = await response.json(); - if (data.errors) { - const flatErrors = data.errors.map((e) => e.message).join(', '); - log.error('error fetching from content API with url', fetchUrl, ':', flatErrors); - throw new Error('Content API responded with error: ' + flatErrors); - } + await handleNotOkResponse(response, fetchUrl); } const data: NeosData = await response.json(); @@ -90,12 +86,7 @@ export const loadPreviewDocumentProps = async ( return undefined; } - const data: ApiErrors = await response.json(); - if (data.errors) { - const flatErrors = data.errors.map((e) => e.message).join(', '); - log.error('error fetching from content API with url', fetchUrl, ':', flatErrors); - throw new Error('Content API responded with error: ' + flatErrors); - } + await handleNotOkResponse(response, fetchUrl); } const data: NeosData = await response.json(); @@ -105,6 +96,25 @@ export const loadPreviewDocumentProps = async ( return data; }; +async function handleNotOkResponse(response: Response, fetchUrl: string): Promise { + try { + const data: ApiErrors = await response.json(); + if (data.errors) { + throw new ApiError('Content API responded with errors', response.status, fetchUrl, data.errors); + } + } catch (e) { + // Ignore any error if response is not JSON + } + + throw new ApiError( + 'Content API responded with unexpected error', + response.status, + fetchUrl, + undefined, + await response.text() + ); +} + export const loadDocumentPropsCached = cache( (routePath: string | undefined, opts?: DataLoaderOptions & OptionalOption) => { if (!routePath) { diff --git a/src/server/utils/hooks.ts b/src/server/utils/hooks.ts index 9f40c9d..effa1b2 100644 --- a/src/server/utils/hooks.ts +++ b/src/server/utils/hooks.ts @@ -79,6 +79,9 @@ export const withSiteNode = async (ctx: ContextProps, opts?: DataLoaderOptions) return neosData.site; }; +/** + * @deprecated Use ctx.inBackend instead (async hooks are not allowed by ESLint rules) + */ export const useInBackend = (ctx: ContextProps) => { return !!ctx?.inBackend; }; diff --git a/test/server/utils/__snapshots__/dataLoader.test.ts.snap b/test/server/utils/__snapshots__/dataLoader.test.ts.snap new file mode 100644 index 0000000..1015203 --- /dev/null +++ b/test/server/utils/__snapshots__/dataLoader.test.ts.snap @@ -0,0 +1,5 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`loadDocumentProps > with NEOS_BASE_URL set > with error not as JSON > should throw an error with the response status text 1`] = `[Error: Content API responded with unexpected error, status: 500, url: http://neos:1234/neos/content-api/document?path=%2Ffoo]`; + +exports[`loadPreviewDocumentProps > with NEOS_BASE_URL set > with Neos session cookie > with error not as JSON > should throw an error with the response status text 1`] = `[Error: Content API responded with unexpected error, status: 500, url: http://neos:1234/neos/content-api/document?contextPath=foo%2Fbar%40user-me]`; diff --git a/test/server/utils/dataLoader.test.ts b/test/server/utils/dataLoader.test.ts index 35d41cc..4f0936f 100644 --- a/test/server/utils/dataLoader.test.ts +++ b/test/server/utils/dataLoader.test.ts @@ -44,6 +44,15 @@ describe('loadDocumentProps', () => { ); }); }); + + describe('with error not as JSON', () => { + it('should throw an error with the response status text', async () => { + const fetch = vi.fn().mockResolvedValue(createNotOkJsonErrorFetchResponse()); + vi.stubGlobal('fetch', fetch); + + await expect(loadDocumentProps({ slug: 'foo' })).rejects.toThrowErrorMatchingSnapshot(); + }); + }); }); describe('with optional option', () => { @@ -131,6 +140,17 @@ describe('loadPreviewDocumentProps', () => { ); }); }); + + describe('with error not as JSON', () => { + it('should throw an error with the response status text', async () => { + const fetch = vi.fn().mockResolvedValue(createNotOkJsonErrorFetchResponse()); + vi.stubGlobal('fetch', fetch); + + await expect( + loadPreviewDocumentProps({ 'node[__contextNodePath]': 'foo/bar@user-me' }) + ).rejects.toThrowErrorMatchingSnapshot(); + }); + }); }); }); }); @@ -218,3 +238,14 @@ afterEach(() => { function createOkayFetchResponse(data: any) { return { ok: true, status: 200, json: () => new Promise((resolve) => resolve(data)) }; } + +function createNotOkJsonErrorFetchResponse() { + return { + ok: false, + status: 500, + json: async () => { + throw new SyntaxError('Unexpected token < in JSON at position 0'); + }, + text: async () => 'Internal Server Error', + }; +} From 33757e9d5f1ae78cc73890910c74a875752d7582 Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Thu, 8 Feb 2024 10:38:02 +0100 Subject: [PATCH 28/39] Import React types properly --- src/lib/components/ContentCollectionProvider.tsx | 6 +++--- src/lib/components/ContentComponent.tsx | 4 +++- src/server/components/ContentCollection.tsx | 3 +-- src/server/components/ContentCollectionProvider.tsx | 13 +++++++------ src/server/components/ContentComponent.tsx | 4 +++- src/server/components/ContentComponentProvider.tsx | 13 +++++++------ src/server/components/Editable.tsx | 10 ++++------ src/server/utils/nodeTypes.ts | 4 +++- src/types/index.ts | 4 +++- tsconfig.json | 6 +----- 10 files changed, 35 insertions(+), 32 deletions(-) diff --git a/src/lib/components/ContentCollectionProvider.tsx b/src/lib/components/ContentCollectionProvider.tsx index 55eef36..99d0d25 100644 --- a/src/lib/components/ContentCollectionProvider.tsx +++ b/src/lib/components/ContentCollectionProvider.tsx @@ -1,4 +1,4 @@ -import { useContext } from 'react'; +import { ReactNode, useContext } from 'react'; import NeosContext from '../../utils/context'; import { useContentCollection } from '../../utils/hooks'; @@ -11,8 +11,8 @@ type ContentCollectionProviderProps = { children, }: { collectionProps: Record; - children: React.ReactNode; - }) => React.ReactNode; + children: ReactNode; + }) => ReactNode; }; export default function ContentCollectionProvider({ nodeName, children }: ContentCollectionProviderProps) { diff --git a/src/lib/components/ContentComponent.tsx b/src/lib/components/ContentComponent.tsx index f521ddc..13c8122 100644 --- a/src/lib/components/ContentComponent.tsx +++ b/src/lib/components/ContentComponent.tsx @@ -1,8 +1,10 @@ +import { ReactNode } from 'react'; + import { useContentComponent } from '../../utils/hooks'; type ContentComponentProps = { as?: keyof JSX.IntrinsicElements; - children: React.ReactNode; + children: ReactNode; [x: string]: any; }; diff --git a/src/server/components/ContentCollection.tsx b/src/server/components/ContentCollection.tsx index ec49423..5318b2d 100644 --- a/src/server/components/ContentCollection.tsx +++ b/src/server/components/ContentCollection.tsx @@ -1,5 +1,4 @@ import { ContextProps } from '../../types'; -import { useInBackend } from '../utils/hooks'; import ContentCollectionProvider from './ContentCollectionProvider'; type ContentCollectionProps = { @@ -10,7 +9,7 @@ type ContentCollectionProps = { }; const ContentCollection = async ({ ctx, as = 'div', nodeName, ...rest }: ContentCollectionProps) => { - const inBackend = useInBackend(ctx); + const inBackend = ctx.inBackend; const { className, ...restAttributes } = rest; const Component = as; diff --git a/src/server/components/ContentCollectionProvider.tsx b/src/server/components/ContentCollectionProvider.tsx index ebb0485..770010f 100644 --- a/src/server/components/ContentCollectionProvider.tsx +++ b/src/server/components/ContentCollectionProvider.tsx @@ -1,6 +1,7 @@ +import { ReactNode } from 'react'; + import { ContextProps } from '../../types'; -import { useInBackend } from '../utils/hooks'; -import { useContentCollection } from '../utils/hooks'; +import { withContentCollection } from '../utils/hooks'; import ContentComponentIncludes from './client/ContentComponentIncludes'; import NodeRenderer from './NodeRenderer'; @@ -12,13 +13,13 @@ type ContentCollectionProviderProps = { children, }: { collectionProps: Record; - children: React.ReactNode; - }) => React.ReactNode; + children: ReactNode; + }) => ReactNode; }; const ContentCollectionProvider = async ({ ctx, nodeName, children }: ContentCollectionProviderProps) => { - const inBackend = useInBackend(ctx); - const { collectionNode, collectionProps } = await useContentCollection(ctx, nodeName)(); + const inBackend = ctx.inBackend; + const { collectionNode, collectionProps } = await withContentCollection(ctx, nodeName); if (!collectionNode) { return null; diff --git a/src/server/components/ContentComponent.tsx b/src/server/components/ContentComponent.tsx index f13cd80..e60b6b1 100644 --- a/src/server/components/ContentComponent.tsx +++ b/src/server/components/ContentComponent.tsx @@ -1,10 +1,12 @@ +import { ReactNode } from 'react'; + import { ContextProps } from '../../types'; import ContentComponentProvider from './ContentComponentProvider'; type ContentComponentProps = { ctx: ContextProps; as?: keyof JSX.IntrinsicElements; - children: React.ReactNode; + children: ReactNode; [x: string]: any; }; diff --git a/src/server/components/ContentComponentProvider.tsx b/src/server/components/ContentComponentProvider.tsx index 0765a55..09fad49 100644 --- a/src/server/components/ContentComponentProvider.tsx +++ b/src/server/components/ContentComponentProvider.tsx @@ -1,6 +1,7 @@ +import { ReactNode } from 'react'; + import { ContextProps } from '../../types'; -import { useInBackend } from '../utils/hooks'; -import { useContentComponent } from '../utils/hooks'; +import { withContentComponent } from '../utils/hooks'; import ContentComponentIncludes from './client/ContentComponentIncludes'; type ContentComponentProviderProps = { @@ -10,13 +11,13 @@ type ContentComponentProviderProps = { includes, }: { componentProps: Record; - includes: React.ReactNode; - }) => React.ReactNode; + includes: ReactNode; + }) => ReactNode; }; const ContentComponentProvider = async ({ ctx, children }: ContentComponentProviderProps) => { - const inBackend = useInBackend(ctx); - const { componentNode, componentProps } = await useContentComponent(ctx)(); + const inBackend = ctx.inBackend; + const { componentNode, componentProps } = await withContentComponent(ctx); if (!componentNode) { return null; diff --git a/src/server/components/Editable.tsx b/src/server/components/Editable.tsx index 57f296e..b18793d 100644 --- a/src/server/components/Editable.tsx +++ b/src/server/components/Editable.tsx @@ -1,5 +1,5 @@ import { ContextProps } from '../../types'; -import { useEditPreviewMode, useInBackend, useNode } from '../utils/hooks'; +import { withEditPreviewMode, withNode } from '../utils/hooks'; type EditableProps = { ctx: ContextProps; @@ -9,12 +9,10 @@ type EditableProps = { }; const Editable = async ({ ctx, as = 'div', property, ...rest }: EditableProps) => { - const inBackend = useInBackend(ctx); - const loadNode = useNode(ctx); - const loadEditPreviewMode = useEditPreviewMode(ctx); + const inBackend = ctx.inBackend; - const node = await loadNode(); - const editPreviewMode = await loadEditPreviewMode(); + const node = await withNode(ctx); + const editPreviewMode = await withEditPreviewMode(ctx); if (!node) { return null; diff --git a/src/server/utils/nodeTypes.ts b/src/server/utils/nodeTypes.ts index ac1f6e7..56ee4ee 100644 --- a/src/server/utils/nodeTypes.ts +++ b/src/server/utils/nodeTypes.ts @@ -1,3 +1,5 @@ +import { FC } from 'react'; + import { ContextProps, NeosNodeTypes } from '../../types'; let nodeTypes: NeosNodeTypes; @@ -6,6 +8,6 @@ export function initNodeTypes(data: NeosNodeTypes) { nodeTypes = data; } -export function getNodeType(nodeTypeName: string): React.FC<{ ctx: ContextProps }> | undefined { +export function getNodeType(nodeTypeName: string): FC<{ ctx: ContextProps }> | undefined { return nodeTypes?.[nodeTypeName]; } diff --git a/src/types/index.ts b/src/types/index.ts index dc6fb17..7dec840 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,5 @@ +import { FC } from 'react'; + declare global { interface Document { __isInitialized?: boolean; @@ -80,7 +82,7 @@ export interface BackendEditPreviewMode { options: Record | null; } -export type NeosNodeTypes = Record>; +export type NeosNodeTypes = Record>; export interface NeosContextProps { node: NeosContentNode; diff --git a/tsconfig.json b/tsconfig.json index ede3c0a..4f97f5b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,11 +15,7 @@ "skipDefaultLibCheck": true, "baseUrl": ".", "outDir": ".", - "rootDir": "src", - "paths": { - "next": ["node_modules/next"], - "react": ["node_modules/react"] - } + "rootDir": "src" }, "include": ["src/**/*"], "exclude": ["./*.js", "./*.d.ts"] From 71095715fe5d76f4ea3eb98f1669de4b2ae4bc94 Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Thu, 8 Feb 2024 15:19:18 +0100 Subject: [PATCH 29/39] Fixed error hanling with bad JSON --- src/server/utils/dataLoader.ts | 91 ++++++++--------- .../__snapshots__/dataLoader.test.ts.snap | 10 ++ test/server/utils/dataLoader.test.ts | 98 +++++++++++++++++-- 3 files changed, 147 insertions(+), 52 deletions(-) diff --git a/src/server/utils/dataLoader.ts b/src/server/utils/dataLoader.ts index 444f70d..cd923c2 100644 --- a/src/server/utils/dataLoader.ts +++ b/src/server/utils/dataLoader.ts @@ -38,17 +38,13 @@ export const loadDocumentProps = async ( next: opts?.next, }); - if (!response.ok) { - if (response.status === 404) { - log.debug('content API returned 404 for path', path); + if (response.status === 404) { + log.debug('content API returned 404 for path', path); - return undefined; - } - - await handleNotOkResponse(response, fetchUrl); + return undefined; } - const data: NeosData = await response.json(); + const data: NeosData = await parseResponse(fetchUrl, response); const endTime = Date.now(); log.debug('fetched data from content API for path', path, ', took', `${endTime - startTime}ms`); @@ -79,42 +75,19 @@ export const loadPreviewDocumentProps = async ( cache: 'no-store', }); - if (!response.ok) { - if (response.status === 404) { - log.debug('content API returned 404 for context path', contextPath); + if (response.status === 404) { + log.debug('content API returned 404 for context path', contextPath); - return undefined; - } - - await handleNotOkResponse(response, fetchUrl); + return undefined; } - const data: NeosData = await response.json(); + const data: NeosData = await parseResponse(fetchUrl, response); const endTime = Date.now(); log.debug('fetched data from content API for context path', contextPath, ', took', `${endTime - startTime}ms`); return data; }; -async function handleNotOkResponse(response: Response, fetchUrl: string): Promise { - try { - const data: ApiErrors = await response.json(); - if (data.errors) { - throw new ApiError('Content API responded with errors', response.status, fetchUrl, data.errors); - } - } catch (e) { - // Ignore any error if response is not JSON - } - - throw new ApiError( - 'Content API responded with unexpected error', - response.status, - fetchUrl, - undefined, - await response.text() - ); -} - export const loadDocumentPropsCached = cache( (routePath: string | undefined, opts?: DataLoaderOptions & OptionalOption) => { if (!routePath) { @@ -170,22 +143,50 @@ export const loadSiteProps = async ( next: opts?.next, }); - if (!response.ok) { - const data: ApiErrors = await response.json(); - if (data.errors) { - const flatErrors = data.errors.map((e) => e.message).join(', '); - log.error('error fetching from content API with url', fetchUrl, ':', flatErrors); - throw new Error('Content API responded with error: ' + flatErrors); - } - } - - const data: CustomSiteData = await response.json(); + const data: CustomSiteData = await parseResponse(fetchUrl, response); const endTime = Date.now(); log.debug('fetched data from content API with url', fetchUrl, ', took', `${endTime - startTime}ms`); return data; }; +async function parseResponse(fetchUrl: string, response: Response): Promise { + if (!response.ok) { + return await handleNotOkResponse(response, fetchUrl); + } + + const responseBody = await response.text(); + try { + return JSON.parse(responseBody); + } catch (e) { + throw new ApiError( + 'Content API responded with invalid JSON: ' + e, + response.status, + fetchUrl, + undefined, + responseBody + ); + } +} + +async function handleNotOkResponse(response: Response, fetchUrl: string): Promise { + const responseBody = await response.text(); + + if (response.headers.get('content-type')?.startsWith('application/json')) { + let data: ApiErrors | undefined = undefined; + try { + data = JSON.parse(responseBody); + } catch (e) { + // Ignore any error if response is not JSON + } + if (data?.errors) { + throw new ApiError('Content API responded with errors', response.status, fetchUrl, data.errors); + } + } + + throw new ApiError('Content API responded with unexpected error', response.status, fetchUrl, undefined, responseBody); +} + export const buildNeosPreviewHeaders = () => { const _headers = nextHeaders(); diff --git a/test/server/utils/__snapshots__/dataLoader.test.ts.snap b/test/server/utils/__snapshots__/dataLoader.test.ts.snap index 1015203..a5c561e 100644 --- a/test/server/utils/__snapshots__/dataLoader.test.ts.snap +++ b/test/server/utils/__snapshots__/dataLoader.test.ts.snap @@ -1,5 +1,15 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`loadDocumentProps > with NEOS_BASE_URL set > with error as JSON > should throw an error with the response status text 1`] = `[Error: Content API responded with errors, status: 500, url: http://neos:1234/neos/content-api/document?path=%2Ffoo, errors: Some error]`; + exports[`loadDocumentProps > with NEOS_BASE_URL set > with error not as JSON > should throw an error with the response status text 1`] = `[Error: Content API responded with unexpected error, status: 500, url: http://neos:1234/neos/content-api/document?path=%2Ffoo]`; +exports[`loadDocumentProps > with NEOS_BASE_URL set > with ok but not as JSON > should throw an error with the response status text 1`] = `[Error: Content API responded with invalid JSON: SyntaxError: Unexpected token '<', " with NEOS_BASE_URL set > with Neos session cookie > with error as JSON > should throw an error with the response status text 1`] = `[Error: Content API responded with errors, status: 500, url: http://neos:1234/neos/content-api/document?contextPath=foo%2Fbar%40user-me, errors: Some error]`; + exports[`loadPreviewDocumentProps > with NEOS_BASE_URL set > with Neos session cookie > with error not as JSON > should throw an error with the response status text 1`] = `[Error: Content API responded with unexpected error, status: 500, url: http://neos:1234/neos/content-api/document?contextPath=foo%2Fbar%40user-me]`; + +exports[`loadSiteProps > with NEOS_BASE_URL set > with error as JSON > should throw an error with the response status text 1`] = `[Error: Content API responded with errors, status: 500, url: http://neos:1234/neos/content-api/site, errors: Some error]`; + +exports[`loadSiteProps > with NEOS_BASE_URL set > with error not as JSON > should throw an error with the response status text 1`] = `[Error: Content API responded with unexpected error, status: 500, url: http://neos:1234/neos/content-api/site]`; diff --git a/test/server/utils/dataLoader.test.ts b/test/server/utils/dataLoader.test.ts index 4f0936f..a93ea6f 100644 --- a/test/server/utils/dataLoader.test.ts +++ b/test/server/utils/dataLoader.test.ts @@ -19,7 +19,10 @@ describe('loadDocumentProps', () => { vi.stubEnv('NEOS_BASE_URL', 'http://neos:1234'); }); - describe.each<{ opts?: DataLoaderOptions; expectedFetchConfig: RequestInit }>([ + describe.each<{ + opts?: DataLoaderOptions; + expectedFetchConfig: RequestInit & { next?: { tags: string[] } | undefined }; + }>([ // No options { expectedFetchConfig: { cache: 'no-store', headers: {}, next: undefined } }, // Specify cache @@ -45,9 +48,31 @@ describe('loadDocumentProps', () => { }); }); + describe('with ok but not as JSON', () => { + it('should throw an error with the response status text', async () => { + const fetch = vi.fn().mockResolvedValue(createOkayButNotJsonFetchResponse()); + vi.stubGlobal('fetch', fetch); + + await expect(loadDocumentProps({ slug: 'foo' })).rejects.toThrowErrorMatchingSnapshot(); + }); + }); + describe('with error not as JSON', () => { it('should throw an error with the response status text', async () => { - const fetch = vi.fn().mockResolvedValue(createNotOkJsonErrorFetchResponse()); + const fetch = vi.fn().mockResolvedValue(createNotOkJsonErrorFetchResponse({ 'Content-Type': 'text/html' })); + vi.stubGlobal('fetch', fetch); + + await expect(loadDocumentProps({ slug: 'foo' })).rejects.toThrowErrorMatchingSnapshot(); + }); + }); + + describe('with error as JSON', () => { + it('should throw an error with the response status text', async () => { + const fetch = vi + .fn() + .mockResolvedValue( + createNotOkJsonErrorFetchResponse({ 'content-type': 'application/json' }, '{"errors":["Some error"]}') + ); vi.stubGlobal('fetch', fetch); await expect(loadDocumentProps({ slug: 'foo' })).rejects.toThrowErrorMatchingSnapshot(); @@ -143,7 +168,22 @@ describe('loadPreviewDocumentProps', () => { describe('with error not as JSON', () => { it('should throw an error with the response status text', async () => { - const fetch = vi.fn().mockResolvedValue(createNotOkJsonErrorFetchResponse()); + const fetch = vi.fn().mockResolvedValue(createNotOkJsonErrorFetchResponse({ 'Content-Type': 'text/html' })); + vi.stubGlobal('fetch', fetch); + + await expect( + loadPreviewDocumentProps({ 'node[__contextNodePath]': 'foo/bar@user-me' }) + ).rejects.toThrowErrorMatchingSnapshot(); + }); + }); + + describe('with error as JSON', () => { + it('should throw an error with the response status text', async () => { + const fetch = vi + .fn() + .mockResolvedValue( + createNotOkJsonErrorFetchResponse({ 'content-type': 'application/json' }, '{"errors":["Some error"]}') + ); vi.stubGlobal('fetch', fetch); await expect( @@ -165,7 +205,12 @@ describe('loadSiteProps', () => { vi.stubEnv('NEOS_BASE_URL', 'http://neos:1234'); }); - describe.each<{ opts?: DataLoaderOptions; expectedFetchConfig: RequestInit }>([ + describe.each<{ + opts?: DataLoaderOptions; + expectedFetchConfig: RequestInit & { + next?: { tags: string[] } | undefined; + }; + }>([ // No options { expectedFetchConfig: { cache: 'no-store', headers: {}, next: undefined } }, // Override cache @@ -182,6 +227,28 @@ describe('loadSiteProps', () => { expect(fetch).toHaveBeenCalledWith('http://neos:1234/neos/content-api/site', expectedFetchConfig); }); }); + + describe('with error not as JSON', () => { + it('should throw an error with the response status text', async () => { + const fetch = vi.fn().mockResolvedValue(createNotOkJsonErrorFetchResponse({ 'Content-Type': 'text/html' })); + vi.stubGlobal('fetch', fetch); + + await expect(loadSiteProps()).rejects.toThrowErrorMatchingSnapshot(); + }); + }); + + describe('with error as JSON', () => { + it('should throw an error with the response status text', async () => { + const fetch = vi + .fn() + .mockResolvedValue( + createNotOkJsonErrorFetchResponse({ 'content-type': 'application/json' }, '{"errors":["Some error"]}') + ); + vi.stubGlobal('fetch', fetch); + + await expect(loadSiteProps()).rejects.toThrowErrorMatchingSnapshot(); + }); + }); }); describe('with optional option', () => { @@ -236,16 +303,33 @@ afterEach(() => { }); function createOkayFetchResponse(data: any) { - return { ok: true, status: 200, json: () => new Promise((resolve) => resolve(data)) }; + return { + ok: true, + status: 200, + json: () => new Promise((resolve) => resolve(data)), + text: () => new Promise((resolve) => resolve(JSON.stringify(data))), + }; +} + +function createOkayButNotJsonFetchResponse() { + return { + ok: true, + status: 200, + json: async () => { + throw new SyntaxError('Unexpected token < in JSON at position 0'); + }, + text: () => new Promise((resolve) => resolve('Some Bad Error')), + }; } -function createNotOkJsonErrorFetchResponse() { +function createNotOkJsonErrorFetchResponse(headers?: HeadersInit, responseBody?: string) { return { ok: false, status: 500, + headers: new Headers(headers), json: async () => { throw new SyntaxError('Unexpected token < in JSON at position 0'); }, - text: async () => 'Internal Server Error', + text: async () => responseBody || 'Internal Server Error', }; } From b25b9144820fcd3a8034f8a4e96853ff8de310c0 Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Thu, 8 Feb 2024 15:29:54 +0100 Subject: [PATCH 30/39] Use Node.js 20 to sync error message --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e3433ec..aa45b56 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: - node-version: '18.x' + node-version: '20.x' cache: yarn - run: yarn install - run: yarn test From 02bd946bc8dfa2e7180ca74b505192ffdf4288ee Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Wed, 21 Feb 2024 12:00:50 +0100 Subject: [PATCH 31/39] Fix an issue where preview data was fetched for all nodes --- src/server/utils/hooks.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/server/utils/hooks.ts b/src/server/utils/hooks.ts index effa1b2..284602d 100644 --- a/src/server/utils/hooks.ts +++ b/src/server/utils/hooks.ts @@ -94,6 +94,10 @@ export const useEditPreviewMode = (ctx: ContextProps, opts?: DataLoaderOptions) }; export const withEditPreviewMode = async (ctx: ContextProps, opts?: DataLoaderOptions) => { + if (!ctx.inBackend) { + return undefined; + } + const neosData = await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts); if (!neosData) { throw new Error(`Document node not found: ${ctx.contextNodePath}`); @@ -110,7 +114,7 @@ export const useContentCollection = (ctx: ContextProps, nodeName?: string, opts? }; export const withContentCollection = async (ctx: ContextProps, nodeName?: string, opts?: DataLoaderOptions) => { - const inBackend = useInBackend(ctx); + const inBackend = ctx.inBackend; const currentNode = await withNode(ctx, opts); @@ -138,7 +142,7 @@ export const useContentComponent = (ctx: ContextProps, opts?: DataLoaderOptions) }; export const withContentComponent = async (ctx: ContextProps, opts?: DataLoaderOptions) => { - const inBackend = useInBackend(ctx); + const inBackend = ctx.inBackend; const node = await withNode(ctx, opts); From b0fb4c975b10066a52a637f9363e3ad5eeeb85cb Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Wed, 21 Feb 2024 15:13:14 +0100 Subject: [PATCH 32/39] Use dataLoaderOptions from ctx for data loaders --- src/server/components/NodeRenderer.tsx | 5 ++--- src/server/utils/hooks.ts | 18 +++++++++--------- src/types/index.ts | 1 + 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/server/components/NodeRenderer.tsx b/src/server/components/NodeRenderer.tsx index f4281bc..134c602 100644 --- a/src/server/components/NodeRenderer.tsx +++ b/src/server/components/NodeRenderer.tsx @@ -10,8 +10,8 @@ type NodeRendererProps = { const NodeRenderer = async ({ ctx, node }: NodeRendererProps) => { // We just fetch again and hope it will be cached const neosData = ctx.inBackend - ? await loadPreviewDocumentPropsCached(ctx.contextNodePath) - : await loadDocumentPropsCached(ctx.routePath); + ? await loadPreviewDocumentPropsCached(ctx.contextNodePath, ctx.dataLoaderOptions) + : await loadDocumentPropsCached(ctx.routePath, ctx.dataLoaderOptions); if (!neosData) { return
Could not load data
; @@ -27,7 +27,6 @@ const NodeRenderer = async ({ ctx, node }: NodeRendererProps) => { diff --git a/src/server/utils/hooks.ts b/src/server/utils/hooks.ts index 284602d..f618875 100644 --- a/src/server/utils/hooks.ts +++ b/src/server/utils/hooks.ts @@ -11,8 +11,8 @@ export const useMeta = (ctx: ContextProps, opts?: DataLoaderOptions & OptionalOp export const withMeta = async (ctx: ContextProps, opts?: DataLoaderOptions) => { const neosData = ctx.inBackend - ? await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts) - : await loadDocumentPropsCached(ctx.routePath, opts); + ? await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts ?? ctx.dataLoaderOptions) + : await loadDocumentPropsCached(ctx.routePath, opts ?? ctx.dataLoaderOptions); if (!neosData) { throw new Error(`Node not found: ${ctx.inBackend ? ctx.contextNodePath : ctx.routePath}`); } @@ -29,8 +29,8 @@ export const useNode = (ctx: ContextProps, opts?: DataLoaderOptions & OptionalOp export const withNode = async (ctx: ContextProps, opts?: DataLoaderOptions) => { const neosData = ctx.inBackend - ? await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts) - : await loadDocumentPropsCached(ctx.routePath, opts); + ? await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts ?? ctx.dataLoaderOptions) + : await loadDocumentPropsCached(ctx.routePath, opts ?? ctx.dataLoaderOptions); if (!neosData) { throw new Error(`Document node not found: ${ctx.inBackend ? ctx.contextNodePath : ctx.routePath}`); } @@ -52,8 +52,8 @@ export const useDocumentNode = (ctx: ContextProps, opts?: DataLoaderOptions) => export const withDocumentNode = async (ctx: ContextProps, opts?: DataLoaderOptions) => { const neosData = ctx.inBackend - ? await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts) - : await loadDocumentPropsCached(ctx.routePath, opts); + ? await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts ?? ctx.dataLoaderOptions) + : await loadDocumentPropsCached(ctx.routePath, opts ?? ctx.dataLoaderOptions); if (!neosData) { throw new Error(`Document node not found: ${ctx.inBackend ? ctx.contextNodePath : ctx.routePath}`); } @@ -70,8 +70,8 @@ export const useSiteNode = (ctx: ContextProps, opts?: DataLoaderOptions) => { export const withSiteNode = async (ctx: ContextProps, opts?: DataLoaderOptions) => { const neosData = ctx.inBackend - ? await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts) - : await loadDocumentPropsCached(ctx.routePath, opts); + ? await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts ?? ctx.dataLoaderOptions) + : await loadDocumentPropsCached(ctx.routePath, opts ?? ctx.dataLoaderOptions); if (!neosData) { throw new Error(`Document node not found: ${ctx.inBackend ? ctx.contextNodePath : ctx.routePath}`); } @@ -98,7 +98,7 @@ export const withEditPreviewMode = async (ctx: ContextProps, opts?: DataLoaderOp return undefined; } - const neosData = await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts); + const neosData = await loadPreviewDocumentPropsCached(ctx.contextNodePath, opts ?? ctx.dataLoaderOptions); if (!neosData) { throw new Error(`Document node not found: ${ctx.contextNodePath}`); } diff --git a/src/types/index.ts b/src/types/index.ts index 7dec840..983de72 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -122,6 +122,7 @@ export type ContextProps = { inBackend?: boolean; documentNodeIdentifier?: string; currentNodeIdentifier?: string; + dataLoaderOptions?: DataLoaderOptions; }; export type DataLoaderOptions = { From fd13ac576975d6ab85cf42668ee2bd423a77fbbf Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Wed, 21 Feb 2024 18:26:04 +0100 Subject: [PATCH 33/39] Fix issue with using next/link when navigating in backend --- src/utils/backendIncludes.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/utils/backendIncludes.ts b/src/utils/backendIncludes.ts index 87bb3cb..b05bc1c 100644 --- a/src/utils/backendIncludes.ts +++ b/src/utils/backendIncludes.ts @@ -8,6 +8,9 @@ export const injectNeosBackendMetadata = (backend: BackendProps | undefined) => createBackendIncludes(backend.guestFrameApplication); } + // Try to unset initialized to perform a re-initialization of the document although it was not unloaded + delete document.__isInitialized; + const event = new CustomEvent('Neos.Neos.Ui.ContentReady'); window.parent.document.dispatchEvent(event); From 649513cff5ab742a4f83afaea734f3d35cbff6b3 Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Thu, 21 Mar 2024 11:21:31 +0100 Subject: [PATCH 34/39] fix: Preserve node and asset URIs in Editable for link editor --- src/lib/components/Editable.tsx | 7 +++++-- src/server/components/Editable.tsx | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/lib/components/Editable.tsx b/src/lib/components/Editable.tsx index 7ce5efa..2088c86 100644 --- a/src/lib/components/Editable.tsx +++ b/src/lib/components/Editable.tsx @@ -7,7 +7,7 @@ type EditableProps = { }; export default function Editable({ as = 'div', property, ...rest }: EditableProps) { - const { properties, nodeType, contextPath } = useNode(); + const { properties, nodeType, contextPath, backend } = useNode(); const inBackend = useInBackend(); const editPreviewMode = useEditPreviewMode(); const { className, ...restAttributes } = rest; @@ -26,7 +26,10 @@ export default function Editable({ as = 'div', property, ...rest }: EditableProp property={'typo3:' + property} data-neos-node-type={nodeType} contentEditable - dangerouslySetInnerHTML={{ __html: properties[property] }} + dangerouslySetInnerHTML={{ + // Use the actual content from the backend metadata if available to preserve original node and asset URIs + __html: backend?.serializedNode?.properties[property] || properties[property] || '', + }} {...restAttributes} /> ); diff --git a/src/server/components/Editable.tsx b/src/server/components/Editable.tsx index b18793d..ce1107c 100644 --- a/src/server/components/Editable.tsx +++ b/src/server/components/Editable.tsx @@ -18,7 +18,7 @@ const Editable = async ({ ctx, as = 'div', property, ...rest }: EditableProps) = return null; } - const { contextPath, nodeType, properties } = node; + const { contextPath, nodeType, properties, backend } = node; const { className, ...restAttributes } = rest; const Component = as; @@ -35,7 +35,10 @@ const Editable = async ({ ctx, as = 'div', property, ...rest }: EditableProps) = property={'typo3:' + property} data-neos-node-type={nodeType} contentEditable - dangerouslySetInnerHTML={{ __html: properties[property] || '' }} + dangerouslySetInnerHTML={{ + // Use the actual content from the backend metadata if available to preserve original node and asset URIs + __html: backend?.serializedNode?.properties[property] || properties[property] || '', + }} {...restAttributes} /> ); From df31fc670bb3dc88fa1c7c4195caa25d6b2380cb Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Tue, 26 Mar 2024 17:24:55 +0100 Subject: [PATCH 35/39] feat: Add types to handle redirects from content API --- src/server/utils/dataLoader.ts | 4 ++-- src/types/index.ts | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/server/utils/dataLoader.ts b/src/server/utils/dataLoader.ts index cd923c2..d1476ec 100644 --- a/src/server/utils/dataLoader.ts +++ b/src/server/utils/dataLoader.ts @@ -2,7 +2,7 @@ import log from 'loglevel'; import { headers as nextHeaders } from 'next/headers'; import { cache } from 'react'; -import { ApiErrors, DataLoaderOptions, NeosData, OptionalOption, SiteData } from '../../types'; +import { ApiErrors, DataLoaderOptions, DocumentResult, NeosData, OptionalOption, SiteData } from '../../types'; import ApiError from './ApiError'; log.setDefaultLevel(log.levels.DEBUG); @@ -44,7 +44,7 @@ export const loadDocumentProps = async ( return undefined; } - const data: NeosData = await parseResponse(fetchUrl, response); + const data: DocumentResult = await parseResponse(fetchUrl, response); const endTime = Date.now(); log.debug('fetched data from content API for path', path, ', took', `${endTime - startTime}ms`); diff --git a/src/types/index.ts b/src/types/index.ts index 983de72..6755fb0 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -6,6 +6,8 @@ declare global { } } +export type DocumentResult = NeosData | RedirectData; + export interface NeosData { node: NeosContentNode; site: NeosNode; @@ -13,6 +15,13 @@ export interface NeosData { backend?: BackendProps; } +export interface RedirectData { + redirect: { + targetPath: string; + statusCode: number; + }; +} + export type SiteData = { site: { content: Record; From 5d90702ab9d03d89d08aa39c38642fa1fe812fa6 Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Tue, 26 Mar 2024 17:31:20 +0100 Subject: [PATCH 36/39] fix: Check the type before using NeosData --- src/server/utils/hooks.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/server/utils/hooks.ts b/src/server/utils/hooks.ts index f618875..390ad2f 100644 --- a/src/server/utils/hooks.ts +++ b/src/server/utils/hooks.ts @@ -17,7 +17,11 @@ export const withMeta = async (ctx: ContextProps, opts?: DataLoaderOptions) => { throw new Error(`Node not found: ${ctx.inBackend ? ctx.contextNodePath : ctx.routePath}`); } - return neosData.meta; + if ('meta' in neosData) { + return neosData.meta; + } + + return undefined; }; /** @@ -34,6 +38,9 @@ export const withNode = async (ctx: ContextProps, opts?: DataLoaderOptions) => { if (!neosData) { throw new Error(`Document node not found: ${ctx.inBackend ? ctx.contextNodePath : ctx.routePath}`); } + if ('redirect' in neosData) { + throw new Error(`Redirect found for node at path: ${ctx.routePath}`); + } const node = resolveCurrentNode(ctx, neosData); if (!node) { @@ -57,6 +64,9 @@ export const withDocumentNode = async (ctx: ContextProps, opts?: DataLoaderOptio if (!neosData) { throw new Error(`Document node not found: ${ctx.inBackend ? ctx.contextNodePath : ctx.routePath}`); } + if ('redirect' in neosData) { + throw new Error(`Redirect found for node at path: ${ctx.routePath}`); + } return neosData.node; }; @@ -75,6 +85,9 @@ export const withSiteNode = async (ctx: ContextProps, opts?: DataLoaderOptions) if (!neosData) { throw new Error(`Document node not found: ${ctx.inBackend ? ctx.contextNodePath : ctx.routePath}`); } + if ('redirect' in neosData) { + throw new Error(`Redirect found for node at path: ${ctx.routePath}`); + } return neosData.site; }; From 480f37402df70403e20ea3e4f70a293fb662434c Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Mon, 29 Apr 2024 12:14:13 +0200 Subject: [PATCH 37/39] Update readme --- README.md | 142 +++++++++++++++++++++++---------------- docs/zebra_logo_dark.svg | 20 ++++++ 2 files changed, 103 insertions(+), 59 deletions(-) create mode 100644 docs/zebra_logo_dark.svg diff --git a/README.md b/README.md index d071c95..f153942 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,74 @@ -[![npm version](https://badge.fury.io/js/@networkteam%2Fzebra.svg)](https://badge.fury.io/js/@networkteam%2Fzebra) -# @networkteam/zebra + -🦓 +

+
+ logo of zebra +
+
+

-## Why? +

@networkteam/zebra

+ +

+A package for Next.js to use Neos CMS as a headless CMS with full visual editing.
+

-* Neos is a great CMS with flexible content structures and focus on a streamlined editing experience -* Next.js offers a great developer experience and a way to build modern websites and applications with a mixed form of static and dynamic pages + +NPM version + +

-So why not combine the best of both worlds? + -Our question was: Can we retain the editing experience of Neos while using Next.js for the frontend? And the answer is: Yes, we can! +## Why? + +* Neos is a great CMS with flexible content structures and focus on a streamlined, visual editing experience. +* Next.js offers a great developer experience and a full-stack framework to build modern websites and applications. ## Features -* No frontend rendering in Neos CMS - it's used headless, besides providing the Neos UI -* Use React components for rendering the frontend based on content (node properties) from Neos - your own components with a few helpers and hooks for editing +* Supports Next.js >= 12.2 with app or pages router +* Full support for content rendering via React server components (RSC) +* Provides components and helpers to load and render content from Neos CMS via React components * Full editing and preview capabilities in the Neos UI using the frontend generated via Next.js * Use multi-language sites with Neos and Next.js * Supports multi-site setups (single Neos with sites, multiple Next.js instances) +* App code and content can be mixed ## How does it work? -This package is used inside a Next.js project that fetches content from Neos CMS for rendering and offers editing with full preview capabilities. It provides components and hooks to handle the rendering of nodes and adding editing metadata for the Neos UI. +This package is used inside a Next.js project that fetches content from Neos CMS for rendering and offers editing with full preview capabilities. It provides components and helpers to handle the loading and rendering of nodes. It adds the necessary editing metadata for the Neos UI to work as in a traditional Neos setup. Inside Neos CMS a few supporting packages are used to provide the content via an API for Next.js and adjust the behavior of the Neos UI: * [Networkteam.Neos.ContentApi](https://github.com/networkteam/Networkteam.Neos.ContentApi) for providing the content via a JSON API. * [Networkteam.Neos.Next](https://github.com/networkteam/Networkteam.Neos.Next) for integrating Next.js as a preview of nodes and handle revalidation of changed documents on publishing. -We also published some supporting tools: - -* [github.com/networkteam/grazer](https://github.com/networkteam/grazer) is an HTTP service implementing a specialized priority queue to revalidate pages for changed documents reliably. - ## Installation +> Note: this readme focuses on using Zebra with the Next.js app router, as it is a more flexible approach for data-loading and supports React server components. + * Create or use an existing Next.js project * Add `@networkteam/zebra` to your project -* Apply `withZebra` to your `next.config.mjs`: +* Apply `withZebra` to your `next.config.js`: ```js - import { withZebra } from '@networkteam/zebra'; + /** @type {import('next').NextConfig} */ + + const { withZebra } = require('@networkteam/zebra'); + + const nextConfig = { + reactStrictMode: true, + // ... + }; - export default withZebra({ - // your next config - }); + module.exports = withZebra(nextConfig); ``` * Create a few pages and an API route for revalidation: - * [`pages/[[...slug]].tsx`](https://github.com/networkteam/zebra-demo/blob/main/next/pages/[[...slug]].tsx) - * [`pages/neos/preview.tsx`](https://github.com/networkteam/zebra-demo/blob/main/next/pages/neos/preview.tsx) - * [`pages/neos/previewNode.tsx`](https://github.com/networkteam/zebra-demo/blob/main/next/pages/neos/previewNode.tsx) - * [`pages/api/revalidate.ts`](https://github.com/networkteam/zebra-demo/blob/main/next/pages/api/revalidate.ts) -* Configure a custom document or add `` to your existing [`pages/_document.tsx`](https://github.com/networkteam/zebra-demo/blob/main/next/pages/_document.tsx) -* Set the environment variable `NEOS_BASE_URL` to your Neos installation + * [`app/[[...slug]]/page.tsx`](https://github.com/networkteam/zebra-demo/blob/main/next/app/[[...slug]]/page.tsx) + * [`app/neos/preview/page.tsx`](https://github.com/networkteam/zebra-demo/blob/main/next/app/neos/preview/page.tsx) + * [`app/neos/previewNode/page.tsx`](https://github.com/networkteam/zebra-demo/blob/main/next/app/neos/previewNode/page.tsx) + * [`app/api/revalidate/route.ts`](https://github.com/networkteam/zebra-demo/blob/main/next/app/api/revalidate/route.ts) +* Set the environment variable `NEOS_BASE_URL` to your Neos installation and `PUBLIC_BASE_URL` to the public URL of your Next.js site. ## Further reading @@ -62,6 +79,7 @@ See the **demo project** for a working example: And here's a list of articles with more background: * [Zebra: Full editing with Neos and Next.js](https://networkteam.com/journal/2023/zebra-neos-and-next) +* [Zebra: A preview of Next.js 13 App Router and React Server Components](https://networkteam.com/journal/2023/zebra-nextjs-app-router-and-server-components) ## Configuration @@ -70,49 +88,51 @@ And here's a list of articles with more background: * `NEOS_BASE_URL`: The base URL of your Neos installation. This could be an internal URL that is not reachable from outside. * `PUBLIC_BASE_URL`: The base URL of your Next.js site. This is the URL where your website will be reachable from outside. * `REVALIDATE_TOKEN`: A secret token that will be used to validate calls to the revalidate API route. -* `REVALIDATE_CONCURRENCY`: How many concurrent revalidations should be performed. Defaults to `2`. ## Rendering content -Zebra provides a `Frontend` component to render a document from Neos. -You provide a mapping from node types to React components. -It is good practice to split components in *presentational* (no knowledge about Neos) and *integrational* components (adds Zebra components and hooks for editing capabilities on top of *presentational* components). +Zebra provides a `NodeRenderer` component to render a node from Neos. +You provide a mapping from node types to React components via `initNodeTypes` which should be imported in your root layout. +It is good practice to split components in *presentational* (no knowledge about Neos) and *content* components (adds Zebra components and helpers for editing capabilities on top of *presentational* components). + +> Note: as React server components do not support `useContext`, we explicitly pass the `ctx` object to all components that need to access content. ### Example -Define node type mappings: +**Define node type mappings (`lib/config/nodeTypes.ts`):** ```tsx +import { initNodeTypes } from '@networkteam/zebra/server'; + import DocumentPage from '../components/document/Page'; import ContentHeadline from '../components/content/Headline'; -export const nodeTypes = { +initNodeTypes({ // Documents 'MyProject.Site:Document.Page': DocumentPage, // Content 'Neos.NodeTypes:Headline': ContentHeadline, -}; +}); ``` -Component for a basic document page: +**Add a component for a basic document page (`lib/components/document/Page.tsx`):** ```tsx -import { - ContentCollection, - withMeta, -} from '@networkteam/zebra/server'; +import { ContextProps } from '@networkteam/zebra'; +import { ContentCollection } from '@networkteam/zebra/server'; import Header from './partials/Header'; -const DocumentPage = async ({ ctx }: { ctx: ContextProps }) => { - const meta = withMeta(ctx); +const DocumentPage = ({ ctx }: { ctx: ContextProps }) => { + const { mainNavigation } = await withMeta(ctx); const inBackend = ctx.inBackend; return (
@@ -126,7 +146,7 @@ const DocumentPage = async ({ ctx }: { ctx: ContextProps }) => { export default DocumentPage; ``` -Presentational component for a headline: +**Add a presentational component for a headline:** ```tsx import classNames from 'classnames'; @@ -161,7 +181,7 @@ const Headline = ({ children, as: Component = 'h1', size, className }: HeadlineP export default Headline; ``` -Integrational component for a headline: +**Add a content component for a headline:** ```tsx import { ContextProps } from '@networkteam/zebra'; @@ -176,7 +196,7 @@ const ContentHeadline = async ({ ctx }: { ctx: ContextProps }) => { return ( - + @@ -186,18 +206,20 @@ const ContentHeadline = async ({ ctx }: { ctx: ContextProps }) => { export default ContentHeadline; ``` -### Static site generation +### Document rendering
This is how the public view of the Next.js site is generated from content in Neos. - Your Next.js project defines a [dynamic catch all route](https://nextjs.org/docs/routing/dynamic-routes#catch-all-routes) that will generate pages for document nodes in Neos CMS. The route is defined in `pages/[[...slug]].tsx`. + Your Next.js project defines a [dynamic route with an optional catch-all segment](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes#optional-catch-all-segments) that will render the page for a document node in Neos CMS. The route is defined in `app/[[...slug]]/page.tsx`. + + Next.js will fetch the node data for a document node from Neos via the Content API in `loadDocumentPropsCached`. You can implement custom processing based on the loaded data, e.g. to fetch additional data, handle shortcuts and redirects or to handle errors. - Next.js will fetch a list of all document nodes from Neos via the Content API in `getStaticProps` for the `[[...slug]]` route. + Using the `NodeRenderer` a new `ctx` prop is passed with information about the current document / node identifier and route path as well as the data loader options for subsequent requests. This will be passed on to all React server components render content. The identifier will be changed while iterating over individual nodes (e.g. content collections). - The `getStaticProps` function will then be called for each page with the `path` and `locale` as params. - The data for the page will be fetched via the Content API in Neos by the path and locale. - This data is the input for rendering the page, so the response of the Content API needs to contain all needed information like menu items, shared content in e.g. a footer and the content of the page itself. + This data is the input for rendering the page, so the response of the Content API needs to contain needed information like menu items, shared content in e.g. a footer and the content of the page itself. With React server components you could also fetch additional data in the component itself. + + Next.js takes care of deduplicating identical fetch requests, so each component in the render tree can fetch data independently without causing additional requests. For this to work, the Neos base URL has to be known to Next.js via the `NEOS_BASE_URL` environment variable.
@@ -211,26 +233,24 @@ export default ContentHeadline; You always access Neos CMS via your Next.js site by appending `/neos`, as usual. The `withZebra` config helper adds the necessary rewrites to the Next.js configuration in `next.config.js` to make this work. - Next.js serves a custom `/neos/preview` route that is used to render the preview of a workspace version of a document node. - It forwards your Neos session cookie to the Neos backend and fetches the content via the Content API - now with access to the user workspace and much more metadata for use in Neos UI. - - This is not done statically - as it would not allow to access the current request and user session - but on demand via `getServerSideProps`. + Next.js serves a custom `/neos/preview` route that is used to render the preview of a document node in the user workspace. + It forwards your Neos session cookie to the Neos backend and fetches the content via the Content API - now with access to the user workspace and more metadata for use in Neos UI. - By using the Zebra components and hooks for rendering, all the metadata for the Neos UI is added to the page. Inline editing should just work. + By using the Zebra components and helpers for rendering, all the metadata for the Neos UI is added to the page. Inline editing should just work. All other requests to `/neos/*` (except `/neos/previewNode`) are proxied to the Neos backend. -### Revalidation +### Caching and revalidation
- This is how incremental static regeneration (ISR) is used if content changes are published in Neos. + This is how on-demand revalidation is used if content changes are published in Neos. - This is done by the [Networkteam.Neos.Next](https://github.com/networkteam/Networkteam.Neos.Next) package in Neos. It hooks into the publishing signals, collects changed nodes and their closest document nodes and triggers a revalidation of the pages via a Next.js API route (defaults to `/api/revalidate`). A revalidate token is used to prevent unauthorized revalidation requests. + This is done by the [Networkteam.Neos.Next](https://github.com/networkteam/Networkteam.Neos.Next) package in Neos. It hooks into the publishing signals, collects changed nodes and their closest document nodes and triggers a revalidation of the routes via a Next.js route handler (defaults to `/api/revalidate`). A revalidate token is used to prevent unauthorized revalidation requests. Note: For this to work, the Next.js base URL has to be known inside Neos. - Since content often depends on other documents (e.g. document titles in navigation, teaser cards, etc.), it is advised to implement a _full revalidation_ after every change. This is why we developed [grazer](https://github.com/networkteam/grazer): it receives revalidate requests from Neos at `/api/revalidate` and handles revalidation requests of all other documents to Next.js in the background. It uses a priority queue that prioritizes older and explicit revalidate route paths before other route paths that are revalidated for consistency. + Since content often depends on other documents (e.g. document titles in navigation, teaser cards, etc.), it is advised to implement a _full revalidation_ after every change. With the app router, this will only mark routes as invalidated and they will be freshly rendered on the next request. Note: This approach works reasonably well and solves a lot of complexity with dependencies and figuring out an _exact_ set of document to revalidate.
@@ -282,6 +302,10 @@ TODO Write more about deployment of a Neos / Next project * Next.js needs to know about the publicly used base URL via the `PUBLIC_BASE_URL` env var to make sure URIs are generated correctly for revalidate calls to the content API in Neos. * Your Neos will need to accept proxy headers from Next.js, make sure to allow it in `Neos.Flow.http.trustedProxies` in `Settings.yaml`. +## Pages router + +Please have a look at https://github.com/networkteam/zebra/blob/v0.9.0/README.md to see instructions for using the pages router. + ## Contributing We are happy to accept contributions. Just open an issue or pull request. diff --git a/docs/zebra_logo_dark.svg b/docs/zebra_logo_dark.svg new file mode 100644 index 0000000..b8c08e4 --- /dev/null +++ b/docs/zebra_logo_dark.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + From 0ee2af5d950be73990ab81bc3edb3ed38707fb28 Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Mon, 29 Apr 2024 14:21:10 +0200 Subject: [PATCH 38/39] Update readme --- README.md | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f153942..7d8c818 100644 --- a/README.md +++ b/README.md @@ -246,7 +246,20 @@ export default ContentHeadline;
This is how on-demand revalidation is used if content changes are published in Neos. - This is done by the [Networkteam.Neos.Next](https://github.com/networkteam/Networkteam.Neos.Next) package in Neos. It hooks into the publishing signals, collects changed nodes and their closest document nodes and triggers a revalidation of the routes via a Next.js route handler (defaults to `/api/revalidate`). A revalidate token is used to prevent unauthorized revalidation requests. + Content can be cached by specifying the data loader options in `loadPreviewDocumentProps`: + + ```typescript + const dataLoaderOptionsFor = (routePath: string): DataLoaderOptions => ({ + cache: 'force-cache', + next: { + tags: ['document'], + }, + }); + + const neosData = await loadDocumentPropsCached(routePath, dataLoaderOptions); + ``` + + It requires the [Networkteam.Neos.Next](https://github.com/networkteam/Networkteam.Neos.Next) package in Neos. It hooks into the publishing signals, collects changed nodes and their closest document nodes and triggers a revalidation of the routes via a Next.js route handler (defaults to `/api/revalidate`). A revalidate token is used to prevent unauthorized revalidation requests. Note: For this to work, the Next.js base URL has to be known inside Neos. @@ -287,15 +300,13 @@ There are multiple things to consider: * The Next.js frontend needs to be built and packaged: * If static pages are pre-built, the Neos CMS deployment has to be finished before the Next.js frontend can be built. - * Another, simpler approach is, to not generate static content here and use an env variable like `CI` to control if static paths / props are fetched from Neos. It works well with `fallback: 'blocking'`, since that will request static props on demand if not yet cached. Bundled with [grazer](https://github.com/networkteam/grazer) an initial revalidation can be performed that will cache all static pages _after Neos and Next are deployed_. -* Next.js needs to be accessible via a public URL, but requests to Neos should also use this URL to generate correct absolute links and resolve sites. - Neos must be accessible form Next.js via another URL - which also could be purely internal (e.g. a Kubernetes Service). - This is why `PUBLIC_BASE_URL` is provided to the Next.js frontend, which will set `X-Forwarded-*` proxy headers for Neos. - Check that your `trustedProxies` configuration in Neos allows this. + * Another, simpler approach is, to not generate static content here and use an env variable like `CI` to opt-out of caching (by calling a function like `headers`). This can be used e.g. in `not-found.tsx` which would cause build errors otherwise, see [`not-found.tsx` in Zebra demo](https://github.com/networkteam/zebra-demo/tree/main/next/app/not-found.tsx). +* Next.js needs to be accessible via a public URL, but requests to Neos should also use this URL to generate correct absolute links and resolve sites: + * Neos must be accessible from Next.js via another URL - which could be purely internal (e.g. a Kubernetes Service). + * This is why `PUBLIC_BASE_URL` is provided to the Next.js frontend, which will set `X-Forwarded-*` proxy headers for Neos. + * Check that your `trustedProxies` configuration in Neos allows this. * Some paths should be routed to Neos (`/neos`, `/_Resources`) and others to Next.js (`/neos/preview`, `/`). In Kubernetes this can be solved at the Ingress level. -TODO Write more about deployment of a Neos / Next project - ### Multi-site caveats * You have to add the publicly used base URL as the primary domain to each site in Neos (via backend module or Flow CLI). @@ -304,7 +315,7 @@ TODO Write more about deployment of a Neos / Next project ## Pages router -Please have a look at https://github.com/networkteam/zebra/blob/v0.9.0/README.md to see instructions for using the pages router. +Please have a look at https://github.com/networkteam/zebra/blob/v0.9.0/README.md to see previous instructions for using the pages router. ## Contributing From 8e7f4f6ef61016c0a0f68e0d329b9da9138a8cd2 Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Mon, 29 Apr 2024 19:33:54 +0200 Subject: [PATCH 39/39] Update readme --- README.md | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/README.md b/README.md index 7d8c818..d45e44a 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,81 @@ initNodeTypes({ }); ``` +**Create a dynamic route with optional catch-all (`app/[[...slug]]/page.tsx`):** + +```tsx +import { loadDocumentPropsCached, NodeRenderer } from '@networkteam/zebra/server'; +import { DataLoaderOptions } from '@networkteam/zebra/types'; +import { Metadata } from 'next'; +import { notFound, redirect } from 'next/navigation'; + +const dataLoaderOptionsFor = (routePath: string): DataLoaderOptions => ({ + cache: 'force-cache', + next: { + tags: ['document'], + }, +}); + +// This is for generating metadata +export async function generateMetadata({ + params, +}: { + params: { + slug: string[]; + }; +}): Promise { + const routePath = params.slug && Array.isArray(params.slug) ? params.slug.join('/') : '/'; + const neosData = await loadDocumentPropsCached(routePath, dataLoaderOptionsFor(routePath)); + if (!neosData) { + return {}; + } + + const { node, site, meta } = neosData; + const title = meta?.isRootPage ? site.properties.title : `${node.properties.title} – ${site.properties.title}`; + return { + title, + }; +} + +// And this will render the page output +const Page = async ({ params: { slug } }: { params: { slug: string[] } }) => { + const routePath = slug && Array.isArray(slug) ? slug.join('/') : '/'; + const dataLoaderOptions = dataLoaderOptionsFor(routePath); + const neosData = await loadDocumentPropsCached(routePath, dataLoaderOptions); + + if (!neosData) { + return notFound(); + } + + // Check for possible redirects + if ('redirect' in neosData) { + if (neosData.redirect.statusCode === 308 || neosData.redirect.statusCode === 301) { + permanentRedirect(neosData.redirect.targetPath); + } + redirect(neosData.redirect.targetPath); + } + + if (neosData?.node.nodeType === 'Neos.Neos:Shortcut') { + return redirect(neosData.node.properties.targetUri || '/'); + } + + return ( + // Render the node data with NodeRenderer which uses the node type mappings + + ); +}; + +export default Page; +``` + **Add a component for a basic document page (`lib/components/document/Page.tsx`):** ```tsx