From c081230212dbcac50f41379cdf734a67da0bbb95 Mon Sep 17 00:00:00 2001 From: RheeseyB <1044774+Rheeseyb@users.noreply.github.com> Date: Tue, 19 Sep 2023 11:18:40 +0100 Subject: [PATCH] Move dependencies from createRemixDerivedData into useGetRoutes hook (#4205) * fix(editor) Move dependencies from createRemixDerivedData into useGetRoutes hook * chore(tests) Updated a remix test to navigate into posts --- .../remix/remix-navigator.spec.browser2.tsx | 14 +- .../remix/remix-rendering.spec.browser2.tsx | 143 +++++++++++------- .../components/canvas/remix/remix-utils.tsx | 79 ++++------ .../remix/utopia-remix-root-component.tsx | 109 ++++++++++++- .../components/editor/store/editor-state.ts | 5 - .../editor/store/remix-derived-data.tsx | 51 ++++--- 6 files changed, 259 insertions(+), 142 deletions(-) diff --git a/editor/src/components/canvas/remix/remix-navigator.spec.browser2.tsx b/editor/src/components/canvas/remix/remix-navigator.spec.browser2.tsx index 946cace86fad..68799353fa76 100644 --- a/editor/src/components/canvas/remix/remix-navigator.spec.browser2.tsx +++ b/editor/src/components/canvas/remix/remix-navigator.spec.browser2.tsx @@ -1,18 +1,26 @@ import * as EP from '../../../core/shared/element-path' import { createModifiedProject } from '../../../sample-projects/sample-project-utils.test-utils' import { setFeatureForBrowserTestsUseInDescribeBlockOnly } from '../../../utils/utils.test-utils' +import { runDOMWalker } from '../../editor/actions/action-creators' import { StoryboardFilePath, navigatorEntryToKey, regularNavigatorEntry, varSafeNavigatorEntryToKey, } from '../../editor/store/editor-state' +import type { PersistentModel } from '../../editor/store/editor-state' import { NavigatorItemTestId } from '../../navigator/navigator-item/navigator-item' import { renderTestEditorWithModel } from '../ui-jsx.test-utils' const DefaultRouteTextContent = 'Hello Remix!' const RootTextContent = 'This is root!' +async function renderRemixProject(project: PersistentModel) { + const renderResult = await renderTestEditorWithModel(project, 'await-first-dom-report') + await renderResult.dispatch([runDOMWalker()], true) + return renderResult +} + describe('Remix navigator', () => { setFeatureForBrowserTestsUseInDescribeBlockOnly('Remix support', true) it('Shows navigator for remix content', async () => { @@ -67,7 +75,7 @@ describe('Remix navigator', () => { `, }) - const renderResult = await renderTestEditorWithModel(project, 'await-first-dom-report') + const renderResult = await renderRemixProject(project) expect(renderResult.getEditorState().derived.navigatorTargets.map(navigatorEntryToKey)).toEqual( [ 'regular-storyboard/remix-scene', @@ -130,7 +138,7 @@ describe('Remix navigator', () => { `, }) - const renderResult = await renderTestEditorWithModel(project, 'await-first-dom-report') + const renderResult = await renderRemixProject(project) expect(renderResult.getEditorState().derived.navigatorTargets.map(navigatorEntryToKey)).toEqual( [ 'regular-storyboard/remix-scene', @@ -194,7 +202,7 @@ describe('Remix navigator', () => { `, }) - const renderResult = await renderTestEditorWithModel(project, 'await-first-dom-report') + const renderResult = await renderRemixProject(project) const navigatorItemElement = renderResult.renderedDOM.getByTestId( NavigatorItemTestId( varSafeNavigatorEntryToKey( diff --git a/editor/src/components/canvas/remix/remix-rendering.spec.browser2.tsx b/editor/src/components/canvas/remix/remix-rendering.spec.browser2.tsx index 3adba5bfab0c..2ef96ca2921b 100644 --- a/editor/src/components/canvas/remix/remix-rendering.spec.browser2.tsx +++ b/editor/src/components/canvas/remix/remix-rendering.spec.browser2.tsx @@ -9,9 +9,10 @@ import { selectComponentsForTest, setFeatureForBrowserTestsUseInDescribeBlockOnly, } from '../../../utils/utils.test-utils' -import { switchEditorMode } from '../../editor/actions/action-creators' +import { runDOMWalker, switchEditorMode } from '../../editor/actions/action-creators' import { EditorModes } from '../../editor/editor-modes' import { StoryboardFilePath, navigatorEntryToKey } from '../../editor/store/editor-state' +import type { PersistentModel } from '../../editor/store/editor-state' import { AddRemoveLayouSystemControlTestId } from '../../inspector/add-remove-layout-system-control' import { CanvasControlsContainerID } from '../controls/new-canvas-controls' import { @@ -56,6 +57,12 @@ export var storyboard = ( /* eslint jest/expect-expect: ["error", { "assertFunctionNames": ["expect", "expectRemixSceneToBeRendered"] }] */ +async function renderRemixProject(project: PersistentModel) { + const renderResult = await renderTestEditorWithModel(project, 'await-first-dom-report') + await renderResult.dispatch([runDOMWalker()], true) + return renderResult +} + describe('Remix content', () => { setFeatureForBrowserTestsUseInDescribeBlockOnly('Remix support', true) it('Renders the remix container with actual content', async () => { @@ -98,7 +105,7 @@ describe('Remix content', () => { `, }) - const renderResult = await renderTestEditorWithModel(project, 'await-first-dom-report') + const renderResult = await renderRemixProject(project) await expectRemixSceneToBeRendered(renderResult) }) @@ -154,7 +161,7 @@ describe('Remix content', () => { `, }) - const renderResult = await renderTestEditorWithModel(project, 'await-first-dom-report') + const renderResult = await renderRemixProject(project) const remixDivMetadata = renderResult.getEditorState().editor.jsxMetadata[ @@ -234,7 +241,7 @@ describe('Remix content', () => { `, }) - const renderResult = await renderTestEditorWithModel(project, 'await-first-dom-report') + const renderResult = await renderRemixProject(project) const remixDivMetadata = renderResult.getEditorState().editor.jsxMetadata[ @@ -318,7 +325,7 @@ describe('Remix content', () => { `, }) - const renderResult = await renderTestEditorWithModel(project, 'await-first-dom-report') + const renderResult = await renderRemixProject(project) const remixDivMetadata = renderResult.getEditorState().editor.jsxMetadata[ @@ -392,7 +399,7 @@ describe('Remix content', () => { `, }) - const renderResult = await renderTestEditorWithModel(project, 'await-first-dom-report') + const renderResult = await renderRemixProject(project) const targetElement = renderResult.renderedDOM.getByTestId('remix-div') const targetElementBounds = targetElement.getBoundingClientRect() @@ -452,7 +459,7 @@ describe('Remix content', () => { `, }) - const renderResult = await renderTestEditorWithModel(project, 'await-first-dom-report') + const renderResult = await renderRemixProject(project) const targetElement = renderResult.renderedDOM.getAllByTestId('remix-div')[1] const targetElementBounds = targetElement.getBoundingClientRect() @@ -521,7 +528,7 @@ describe('Remix content', () => { `, }) - const renderResult = await renderTestEditorWithModel(project, 'await-first-dom-report') + const renderResult = await renderRemixProject(project) const targetElement = renderResult.renderedDOM.getByTestId(DraggedElementId) const targetElementBounds = targetElement.getBoundingClientRect() @@ -543,7 +550,7 @@ describe('Remix content with feature switch off', () => { const project = createModifiedProject({ [StoryboardFilePath]: storyboardFileContent, }) - const renderResult = await renderTestEditorWithModel(project, 'await-first-dom-report') + const renderResult = await renderRemixProject(project) await expect(async () => renderResult.renderedDOM.findAllByTestId(REMIX_SCENE_TESTID), ).rejects.toThrow() @@ -588,28 +595,80 @@ describe('Remix navigation', () => { import { Link } from '@remix-run/react' export default function Index() { - return ${DefaultRouteTextContent} + return ${DefaultRouteTextContent} } `, - ['/src/routes/about.js']: `import React from 'react' - - export default function About() { - return

About

+ ['/src/routes/posts._index.js']: `import React from 'react' + import { Link } from '@remix-run/react' + import { json, useLoaderData } from 'react-router' + + export function loader() { + return json({ + activities: [ + { + id: 0, + name: 'Do the thing', + }, + ] + }) + } + + export default function Posts() { + const { activities } = useLoaderData() + return ( +
+ {activities.map( + ({ + id, + name, + }) => ( + {name} + ) + )} +
+ ) + } + `, + ['/src/routes/posts.$postId.js']: `import React from 'react' + import { json, useLoaderData } from 'react-router' + + export function loader({ params }) { + return json({ + id: 0, + name: 'Do the thing', + desc: 'Do it now!' + }) + } + + export default function Post() { + const { + desc + } = useLoaderData() + + return ( +
{desc}
+ ) } `, }) - const renderResult = await renderTestEditorWithModel(project, 'await-first-dom-report') + const renderResult = await renderRemixProject(project) await renderResult.dispatch([switchEditorMode(EditorModes.liveMode())], true) const targetElement = renderResult.renderedDOM.getByTestId('remix-link') const targetElementBounds = targetElement.getBoundingClientRect() - const clickPoint = windowPoint({ x: targetElementBounds.x + 5, y: targetElementBounds.y + 5 }) + await mouseClickAtPoint(targetElement, { + x: targetElementBounds.x + 5, + y: targetElementBounds.y + 5, + }) - await mouseClickAtPoint(targetElement, clickPoint) + const postLink = renderResult.renderedDOM.getByTestId('post-link') + const postLinkBounds = postLink.getBoundingClientRect() - await expectRemixSceneToBeRendered(renderResult, 'About') + await mouseClickAtPoint(postLink, { x: postLinkBounds.x + 5, y: postLinkBounds.y + 5 }) + + await expectRemixSceneToBeRendered(renderResult, 'Do it now!') }) it('Remix navigation updates metadata', async () => { @@ -671,7 +730,8 @@ describe('Remix navigation', () => { `, }) - const renderResult = await renderTestEditorWithModel(project, 'await-first-dom-report') + const renderResult = await renderRemixProject(project) + await renderResult.dispatch([switchEditorMode(EditorModes.liveMode())], true) const remixLinkMetadata = renderResult.getEditorState().editor.jsxMetadata[ @@ -679,8 +739,6 @@ describe('Remix navigation', () => { ] expect(remixLinkMetadata).not.toBeUndefined() - await renderResult.dispatch([switchEditorMode(EditorModes.liveMode())], true) - const targetElement = renderResult.renderedDOM.getByTestId('remix-link') const targetElementBounds = targetElement.getBoundingClientRect() @@ -753,7 +811,7 @@ describe('Editing Remix content', () => { `, }) - const renderResult = await renderTestEditorWithModel(project, 'await-first-dom-report') + const renderResult = await renderRemixProject(project) const pathString = 'sb/rs:root/outlet:title' @@ -936,7 +994,7 @@ describe('Editing Remix content', () => { `, }) - const renderResult = await renderTestEditorWithModel(project, 'await-first-dom-report') + const renderResult = await renderRemixProject(project) const pathString = 'sb/rs:root/outlet2:title' @@ -949,10 +1007,7 @@ describe('Editing Remix content', () => { }) it('delete element from remix scene', async () => { - const renderResult = await renderTestEditorWithModel( - remixProjectForEditingTests, - 'await-first-dom-report', - ) + const renderResult = await renderRemixProject(remixProjectForEditingTests) expect(renderResult.renderedDOM.queryAllByTestId(FlexDivTestId)).toHaveLength(1) @@ -999,10 +1054,7 @@ export default function Index() { }) it('use the inspector to add a layout system', async () => { - const renderResult = await renderTestEditorWithModel( - remixProjectForEditingTests, - 'await-first-dom-report', - ) + const renderResult = await renderRemixProject(remixProjectForEditingTests) const absoluteDiv = await clickElementOnCanvas(renderResult, AbsoluteDivTestId) @@ -1013,10 +1065,9 @@ export default function Index() { }) it('flex reorder elements inside Remix', async () => { - const renderResult = await renderTestEditorWithModel( - remixProjectForEditingTests, - 'await-first-dom-report', - ) + const renderResult = await renderRemixProject(remixProjectForEditingTests) + + await renderResult.dispatch([runDOMWalker()], true) expect(renderResult.getEditorState().derived.navigatorTargets.map(navigatorEntryToKey)).toEqual( [ @@ -1056,10 +1107,7 @@ export default function Index() { }) it('absolute move elements inside Remix', async () => { - const renderResult = await renderTestEditorWithModel( - remixProjectForEditingTests, - 'await-first-dom-report', - ) + const renderResult = await renderRemixProject(remixProjectForEditingTests) const absoluteDiv = await clickElementOnCanvas(renderResult, AbsoluteDivTestId) expect({ left: absoluteDiv.style.left, top: absoluteDiv.style.top }).toEqual({ @@ -1081,10 +1129,9 @@ export default function Index() { }) it('draw to insert into Remix', async () => { - const renderResult = await renderTestEditorWithModel( - remixProjectForEditingTests, - 'await-first-dom-report', - ) + const renderResult = await renderRemixProject(remixProjectForEditingTests) + + await renderResult.dispatch([runDOMWalker()], true) expect(renderResult.getEditorState().derived.navigatorTargets.map(navigatorEntryToKey)).toEqual( [ @@ -1203,10 +1250,7 @@ export default function Index() { const clipboardMock = new MockClipboardHandlers().mock() it('copy-paste element inside Remix', async () => { - const renderResult = await renderTestEditorWithModel( - remixProjectForEditingTests, - 'await-first-dom-report', - ) + const renderResult = await renderRemixProject(remixProjectForEditingTests) await selectComponentsForTest(renderResult, [ EP.fromString('sb/remix-scene:app/outlet:index/absolute-div'), @@ -1310,10 +1354,7 @@ export default function Index() { }) it('dragging elements between Remix and the storyboard', async () => { - const renderResult = await renderTestEditorWithModel( - remixProjectForEditingTests, - 'await-first-dom-report', - ) + const renderResult = await renderRemixProject(remixProjectForEditingTests) expect(renderResult.getEditorState().derived.navigatorTargets.map(navigatorEntryToKey)).toEqual( [ 'regular-sb/remix-scene', diff --git a/editor/src/components/canvas/remix/remix-utils.tsx b/editor/src/components/canvas/remix/remix-utils.tsx index 940229fd9a62..ea6be7fdebf3 100644 --- a/editor/src/components/canvas/remix/remix-utils.tsx +++ b/editor/src/components/canvas/remix/remix-utils.tsx @@ -139,7 +139,7 @@ export function createAssetsManifest(routes: RouteManifest): AssetsM export interface RouteModuleCreator { filePath: string - executionScopeCreator: (projectContents: ProjectContentTreeRoot) => ExecutionScope + executionScopeCreator: ExecutionScopeCreator } export interface RouteIdsToModuleCreators { @@ -160,22 +160,6 @@ export interface GetRoutesAndModulesFromManifestResult { routingTable: RemixRoutingTable } -function addLoaderAndActionToRoutes( - routes: DataRouteObject[], - routeId: string, - loader: LoaderFunction | undefined, - action: ActionFunction | undefined, -) { - routes.forEach((route) => { - if (route.id === routeId) { - route.action = action - route.loader = loader - } else { - addLoaderAndActionToRoutes(route.children ?? [], routeId, loader, action) - } - }) -} - function getRouteModulesWithPaths( projectContents: ProjectContentTreeRoot, manifest: RouteManifest, @@ -235,32 +219,41 @@ function getRouteModulesWithPaths( return routeModulesWithBasePaths } +export type ExecutionScopeCreator = ( + innerProjectContents: ProjectContentTreeRoot, + fileBlobs: CanvasBase64Blobs, + hiddenInstances: Array, + displayNoneInstances: Array, + metadataContext: UiJsxCanvasContextData, +) => ExecutionScope + function getRemixExportsOfModule( filename: string, curriedRequireFn: CurriedUtopiaRequireFn, curriedResolveFn: CurriedResolveFn, - metadataContext: UiJsxCanvasContextData, projectContents: ProjectContentTreeRoot, - mutableContextRef: React.MutableRefObject, - topLevelComponentRendererComponents: React.MutableRefObject< - MapLike> - >, - fileBlobs: CanvasBase64Blobs, - hiddenInstances: Array, - displayNoneInstances: Array, ): { - executionScopeCreator: (innerProjectContents: ProjectContentTreeRoot) => ExecutionScope - loader: LoaderFunction | undefined - action: ActionFunction | undefined + executionScopeCreator: ExecutionScopeCreator rootComponentUid: string } { - const executionScopeCreator = (innerProjectContents: ProjectContentTreeRoot) => { + const executionScopeCreator = ( + innerProjectContents: ProjectContentTreeRoot, + fileBlobs: CanvasBase64Blobs, + hiddenInstances: Array, + displayNoneInstances: Array, + metadataContext: UiJsxCanvasContextData, + ) => { let resolvedFiles: MapLike> = {} let resolvedFileNames: Array = ['/src/root.js'] const requireFn = curriedRequireFn(innerProjectContents) const resolve = curriedResolveFn(innerProjectContents) + let mutableContextRef: { current: MutableUtopiaCtxRefData } = { current: {} } + let topLevelComponentRendererComponents: { + current: MapLike> + } = { current: {} } + const customRequire = (importOrigin: string, toImport: string) => { if (resolvedFiles[importOrigin] == null) { resolvedFiles[importOrigin] = [] @@ -281,9 +274,9 @@ function getRemixExportsOfModule( mutableContextRef, topLevelComponentRendererComponents, '/src/root.js', - {}, - [], - [], + fileBlobs, + hiddenInstances, + displayNoneInstances, metadataContext, NO_OP, false, @@ -319,16 +312,11 @@ function getRemixExportsOfModule( ) } - const executionScope = executionScopeCreator(projectContents) - const nameAndUid = getDefaultExportNameAndUidFromFile(projectContents, filename) return { executionScopeCreator: executionScopeCreator, rootComponentUid: nameAndUid?.uid ?? 'NO-ROOT', - // FIXME the executionScope should be created at the point where we use the loader and action like we do for the module's default export component - loader: executionScope.scope['loader'] as LoaderFunction | undefined, - action: executionScope.scope['action'] as ActionFunction | undefined, } } export function getRoutesAndModulesFromManifest( @@ -336,16 +324,8 @@ export function getRoutesAndModulesFromManifest( futureConfig: FutureConfig, curriedRequireFn: CurriedUtopiaRequireFn, curriedResolveFn: CurriedResolveFn, - metadataContext: UiJsxCanvasContextData, projectContents: ProjectContentTreeRoot, - mutableContextRef: React.MutableRefObject, - topLevelComponentRendererComponents: React.MutableRefObject< - MapLike> - >, routeModulesCache: RouteModules, - fileBlobs: CanvasBase64Blobs, - hiddenInstances: Array, - displayNoneInstances: Array, ): GetRoutesAndModulesFromManifestResult | null { const routeModuleCreators: RouteIdsToModuleCreators = {} const routingTable: RemixRoutingTable = {} @@ -381,20 +361,13 @@ export function getRoutesAndModulesFromManifest( ) Object.values(routeManifest).forEach((route) => { - const { executionScopeCreator, loader, action, rootComponentUid } = getRemixExportsOfModule( + const { executionScopeCreator, rootComponentUid } = getRemixExportsOfModule( route.module, curriedRequireFn, curriedResolveFn, - metadataContext, projectContents, - mutableContextRef, - topLevelComponentRendererComponents, - fileBlobs, - hiddenInstances, - displayNoneInstances, ) - addLoaderAndActionToRoutes(routes, route.id, loader, action) routeModuleCreators[route.id] = { filePath: route.module, executionScopeCreator: executionScopeCreator, diff --git a/editor/src/components/canvas/remix/utopia-remix-root-component.tsx b/editor/src/components/canvas/remix/utopia-remix-root-component.tsx index ad95041bc70d..610c54e71a05 100644 --- a/editor/src/components/canvas/remix/utopia-remix-root-component.tsx +++ b/editor/src/components/canvas/remix/utopia-remix-root-component.tsx @@ -1,7 +1,7 @@ import { RemixContext } from '@remix-run/react/dist/components' import type { RouteModules } from '@remix-run/react/dist/routeModules' import React from 'react' -import type { Location } from 'react-router' +import type { DataRouteObject, Location } from 'react-router' import { createMemoryRouter, RouterProvider } from 'react-router' import { UTOPIA_PATH_KEY } from '../../../core/model/utopia-constants' import type { ElementPath } from '../../../core/shared/project-file-types' @@ -11,6 +11,10 @@ import { PathPropHOC } from './path-props-hoc' import { atom, useAtom, useSetAtom } from 'jotai' import { getDefaultExportNameAndUidFromFile } from '../../../core/model/project-file-utils' import { OutletPathContext } from './remix-utils' +import { UiJsxCanvasCtxAtom } from '../ui-jsx-canvas' +import type { UiJsxCanvasContextData } from '../ui-jsx-canvas' +import { forceNotNull } from '../../../core/shared/optional-utils' +import { AlwaysFalse, usePubSubAtomReadOnly } from '../../../core/shared/atom-with-pub-sub' type RouterType = ReturnType @@ -32,6 +36,14 @@ export const RemixNavigationAtom = atom({}) function useGetRouteModules(basePath: ElementPath) { const remixDerivedDataRef = useRefEditorState((store) => store.derived.remixData) const projectContentsRef = useRefEditorState((store) => store.editor.projectContents) + const fileBlobsRef = useRefEditorState((store) => store.editor.canvas.base64Blobs) + const hiddenInstancesRef = useRefEditorState((store) => store.editor.hiddenInstances) + const displayNoneInstancesRef = useRefEditorState((store) => store.editor.displayNoneInstances) + + let metadataContext: UiJsxCanvasContextData = forceNotNull( + `Missing UiJsxCanvasCtxAtom provider`, + usePubSubAtomReadOnly(UiJsxCanvasCtxAtom, AlwaysFalse), + ) const defaultExports = useEditorState( Substores.derived, @@ -65,7 +77,13 @@ function useGetRouteModules(basePath: ElementPath) { const defaultComponent = (componentProps: any) => value - .executionScopeCreator(projectContentsRef.current) + .executionScopeCreator( + projectContentsRef.current, + fileBlobsRef.current, + hiddenInstancesRef.current, + displayNoneInstancesRef.current, + metadataContext, + ) .scope[nameAndUid.name]?.(componentProps) ?? routeModulesResult[routeId] = { @@ -75,7 +93,86 @@ function useGetRouteModules(basePath: ElementPath) { } return routeModulesResult - }, [defaultExports, remixDerivedDataRef, projectContentsRef]) + }, [ + defaultExports, + metadataContext, + remixDerivedDataRef, + projectContentsRef, + fileBlobsRef, + hiddenInstancesRef, + displayNoneInstancesRef, + ]) +} + +function useGetRoutes() { + const routes = useEditorState( + Substores.derived, + (store) => store.derived.remixData?.routes ?? [], + 'UtopiaRemixRootComponent routes', + ) + + const remixDerivedDataRef = useRefEditorState((store) => store.derived.remixData) + const projectContentsRef = useRefEditorState((store) => store.editor.projectContents) + const fileBlobsRef = useRefEditorState((store) => store.editor.canvas.base64Blobs) + const hiddenInstancesRef = useRefEditorState((store) => store.editor.hiddenInstances) + const displayNoneInstancesRef = useRefEditorState((store) => store.editor.displayNoneInstances) + + let metadataContext: UiJsxCanvasContextData = forceNotNull( + `Missing UiJsxCanvasCtxAtom provider`, + usePubSubAtomReadOnly(UiJsxCanvasCtxAtom, AlwaysFalse), + ) + + return React.useMemo(() => { + if (remixDerivedDataRef.current == null) { + return routes + } + + const creators = remixDerivedDataRef.current.routeModuleCreators + + function addLoaderAndActionToRoutes(innerRoutes: DataRouteObject[]) { + innerRoutes.forEach((route) => { + // FIXME Adding a loader function to the 'root' route causes the `createShouldRevalidate` to fail, because + // we only ever pass in an empty object for the `routeModules` and never mutate it + const creatorForRoute = route.id === 'root' ? null : creators[route.id] + if (creatorForRoute != null) { + route.action = (args: any) => + creatorForRoute + .executionScopeCreator( + projectContentsRef.current, + fileBlobsRef.current, + hiddenInstancesRef.current, + displayNoneInstancesRef.current, + metadataContext, + ) + .scope['action']?.(args) ?? null + route.loader = (args: any) => + creatorForRoute + .executionScopeCreator( + projectContentsRef.current, + fileBlobsRef.current, + hiddenInstancesRef.current, + displayNoneInstancesRef.current, + metadataContext, + ) + .scope['loader']?.(args) ?? null + } + + addLoaderAndActionToRoutes(route.children ?? []) + }) + } + + addLoaderAndActionToRoutes(routes) + + return routes + }, [ + displayNoneInstancesRef, + metadataContext, + fileBlobsRef, + hiddenInstancesRef, + projectContentsRef, + remixDerivedDataRef, + routes, + ]) } export interface UtopiaRemixRootComponentProps { @@ -85,11 +182,7 @@ export interface UtopiaRemixRootComponentProps { export const UtopiaRemixRootComponent = React.memo((props: UtopiaRemixRootComponentProps) => { const remixDerivedDataRef = useRefEditorState((store) => store.derived.remixData) - const routes = useEditorState( - Substores.derived, - (store) => store.derived.remixData?.routes ?? [], - 'UtopiaRemixRootComponent routes', - ) + const routes = useGetRoutes() const basePath = props[UTOPIA_PATH_KEY] diff --git a/editor/src/components/editor/store/editor-state.ts b/editor/src/components/editor/store/editor-state.ts index ace31577a4db..3f5166c6a51c 100644 --- a/editor/src/components/editor/store/editor-state.ts +++ b/editor/src/components/editor/store/editor-state.ts @@ -2598,11 +2598,6 @@ export function deriveState( const remixDerivedData = createRemixDerivedDataMemo( editor.projectContents, - editor.spyMetadata, - editor.allElementProps, - editor.canvas.base64Blobs, - editor.hiddenInstances, - editor.displayNoneInstances, editor.codeResultCache.curriedRequireFn, editor.codeResultCache.curriedResolveFn, ) diff --git a/editor/src/components/editor/store/remix-derived-data.tsx b/editor/src/components/editor/store/remix-derived-data.tsx index 76bec4b86752..13de2362899e 100644 --- a/editor/src/components/editor/store/remix-derived-data.tsx +++ b/editor/src/components/editor/store/remix-derived-data.tsx @@ -7,6 +7,7 @@ import type { MutableUtopiaCtxRefData } from '../../canvas/ui-jsx-canvas-rendere import type { MapLike } from 'typescript' import type { ComponentRendererComponent } from '../../canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-component-renderer' import type { DataRouteObject } from 'react-router' +import { isProjectContentDirectory, isProjectContentFile } from '../../assets' import type { ProjectContentTreeRoot } from '../../assets' import type { RouteIdsToModuleCreators, @@ -19,10 +20,8 @@ import { getRoutesAndModulesFromManifest, } from '../../canvas/remix/remix-utils' import type { CurriedUtopiaRequireFn, CurriedResolveFn } from '../../custom-code/code-file' -import type { ElementPath } from '../../../core/shared/project-file-types' -import type { ElementInstanceMetadataMap } from '../../../core/shared/element-template' -import type { AllElementProps, CanvasBase64Blobs } from './editor-state' import { memoize } from '../../../core/shared/memoize' +import { shallowEqual } from '../../../core/shared/equality-utils' export interface RemixRoutingTable { [rootElementUid: string]: string /* file path */ @@ -47,13 +46,10 @@ const CreateRemixDerivedDataRefs: { routeModulesCache: { current: {} }, } +// Important Note: When updating the params here, you must evaluate whether the change should +// have an effect on the memoization, and if so update paramsEqualityFn below export function createRemixDerivedData( projectContents: ProjectContentTreeRoot, - spyMetadata: ElementInstanceMetadataMap, - allElementProps: AllElementProps, - fileBlobs: CanvasBase64Blobs, - displayNoneInstances: Array, - hiddenInstances: Array, curriedRequireFn: CurriedUtopiaRequireFn, curriedResolveFn: CurriedResolveFn, ): RemixDerivedData | null { @@ -64,25 +60,13 @@ export function createRemixDerivedData( const assetsManifest = createAssetsManifest(routeManifest) - const metadataCtx = { - current: { - spyValues: { metadata: spyMetadata, allElementProps: allElementProps }, - }, - } - const routesAndModulesFromManifestResult = getRoutesAndModulesFromManifest( routeManifest, DefaultFutureConfig, curriedRequireFn, curriedResolveFn, - metadataCtx, projectContents, - CreateRemixDerivedDataRefs.mutableContext, - CreateRemixDerivedDataRefs.topLevelComponentRendererComponents, CreateRemixDerivedDataRefs.routeModulesCache.current, - fileBlobs, - displayNoneInstances, - hiddenInstances, ) if (routesAndModulesFromManifestResult == null) { @@ -102,8 +86,31 @@ export function createRemixDerivedData( } } -export const patchedCreateRemixDerivedDataMemo = memoize(createRemixDerivedData, { maxSize: 1 }) +function isProjectContentTreeRoot(v: unknown): v is ProjectContentTreeRoot { + if (v != null && typeof v === 'object' && !Array.isArray(v)) { + const firstValue = Object.values(v)[0] + return isProjectContentDirectory(firstValue) || isProjectContentFile(firstValue) + } + + return false +} + +function paramsEqualityFn(l: unknown, r: unknown): boolean { + if (isProjectContentTreeRoot(l) && isProjectContentTreeRoot(r)) { + return shallowEqual(l, r) + } + + return l === r +} + +export const patchedCreateRemixDerivedDataMemo = memoize(createRemixDerivedData, { + maxSize: 1, + matchesArg: paramsEqualityFn, +}) -export const unpatchedCreateRemixDerivedDataMemo = memoize(createRemixDerivedData, { maxSize: 1 }) +export const unpatchedCreateRemixDerivedDataMemo = memoize(createRemixDerivedData, { + maxSize: 1, + matchesArg: paramsEqualityFn, +}) export type RemixDerivedDataFactory = typeof createRemixDerivedData