diff --git a/editor/package.json b/editor/package.json
index be46a465af06..94d15955d118 100644
--- a/editor/package.json
+++ b/editor/package.json
@@ -115,6 +115,7 @@
"@liveblocks/react-comments": "1.7.1",
"@liveblocks/yjs": "1.7.1",
"@popperjs/core": "2.4.4",
+ "@shopify/remix-oxygen": "2.0.1",
"@remix-run/react": "2.0.1",
"@remix-run/server-runtime": "2.3.1",
"@root/encoding": "1.0.1",
diff --git a/editor/pnpm-lock.yaml b/editor/pnpm-lock.yaml
index ead24f651554..e6520ada2eff 100644
--- a/editor/pnpm-lock.yaml
+++ b/editor/pnpm-lock.yaml
@@ -75,6 +75,7 @@ specifiers:
'@root/encoding': 1.0.1
'@seznam/compose-react-refs': ^1.0.4
'@shopify/hydrogen': 2023.10.2
+ '@shopify/remix-oxygen': 2.0.1
'@stitches/react': 1.2.8
'@svgr/plugin-jsx': 5.5.0
'@testing-library/react': 14.0.0
@@ -363,6 +364,7 @@ dependencies:
'@root/encoding': 1.0.1
'@seznam/compose-react-refs': 1.0.6
'@shopify/hydrogen': 2023.10.2_w6lhquyj4jtuytgedwpbhq3tte
+ '@shopify/remix-oxygen': 2.0.1_7d3z4mwwjazrsw5sc36gfmw2te
'@stitches/react': 1.2.8_react@18.1.0
'@svgr/plugin-jsx': 5.5.0
'@tippyjs/react': 4.1.0_ef5jwxihqo6n7gxfmzogljlgcm
@@ -4710,6 +4712,18 @@ packages:
- xstate
dev: false
+ /@shopify/remix-oxygen/2.0.1_7d3z4mwwjazrsw5sc36gfmw2te:
+ resolution: {integrity: sha512-D1dqaAr0P9kARqqh2OKiNW/SglCq3FWX0NdJ/RTrwwQMuwcrKWKJAOL/bxmIbYXVYfY0YsBdhvq3bIbb0KpRog==}
+ peerDependencies:
+ '@remix-run/server-runtime': ^2.1.0
+ '@shopify/oxygen-workers-types': ^3.17.3
+ peerDependenciesMeta:
+ '@remix-run/server-runtime':
+ optional: true
+ dependencies:
+ '@remix-run/server-runtime': 2.3.1_typescript@5.2.2
+ dev: false
+
/@sindresorhus/is/4.2.0:
resolution: {integrity: sha512-VkE3KLBmJwcCaVARtQpfuKcKv8gcBmUubrfHGF84dXuuW6jgsRYxPtzcIhPyK9WAPpRt2/xY6zkD9MnRaJzSyw==}
engines: {node: '>=10'}
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 277c4bb7a180..8c411a996d72 100644
--- a/editor/src/components/canvas/remix/remix-rendering.spec.browser2.tsx
+++ b/editor/src/components/canvas/remix/remix-rendering.spec.browser2.tsx
@@ -797,6 +797,77 @@ describe('Remix content', () => {
})
})
+describe('Remix getLoadContext', () => {
+ it('Renders the remix project that relies on loader context', async () => {
+ const project = createModifiedProject({
+ [StoryboardFilePath]: `import * as React from 'react'
+ import { RemixScene, Storyboard } from 'utopia-api'
+
+ const getLoadContext = async (request) => {
+ return {
+ request: request,
+ otherStuff: 'hello!'
+ }
+ }
+
+ export var storyboard = (
+
+
+
+ )
+ `,
+ ['/app/root.js']: `import React from 'react'
+ import { Outlet } from '@remix-run/react'
+ import { json, useLoaderData } from 'react-router'
+
+ export function loader({params, request, context}) {
+ console.log('loader called', params, request, context)
+ return json({
+ contextEqualsRequest: context.request === request,
+ })
+ }
+
+ export default function Root() {
+ const loaderData = useLoaderData()
+
+ return (
+
+ Request given to loader matches the request given to getLoadContext: {JSON.stringify(loaderData.contextEqualsRequest)}
+
${RootTextContent}
+
+
+ )
+ }
+ `,
+ ['/app/routes/_index.js']: `import React from 'react'
+
+ const Index = () => (${DefaultRouteTextContent}
)
+ export default Index
+ `,
+ })
+
+ const renderResult = await renderRemixProject(project)
+
+ expect(
+ renderResult.renderedDOM.getByText(
+ 'Request given to loader matches the request given to getLoadContext: true',
+ ),
+ ).toBeTruthy()
+
+ await expectRemixSceneToBeRendered(renderResult)
+ })
+})
+
describe('Remix navigation', () => {
const projectWithMultipleRoutes = () =>
createModifiedProject({
diff --git a/editor/src/components/canvas/remix/remix-utils.tsx b/editor/src/components/canvas/remix/remix-utils.tsx
index 674f32ffc7a1..48e6676ba1b8 100644
--- a/editor/src/components/canvas/remix/remix-utils.tsx
+++ b/editor/src/components/canvas/remix/remix-utils.tsx
@@ -41,6 +41,10 @@ import { getAllUniqueUids } from '../../../core/model/get-unique-ids'
import { safeIndex } from '../../../core/shared/array-utils'
import { createClientRoutes, groupRoutesByParentId } from '../../../third-party/remix/client-routes'
import path from 'path'
+import {
+ type CustomServerJSExecutor,
+ getCustomServerJSExecutor,
+} from '../../../core/es-modules/package-manager/hydrogen-oxygen-support'
export const OutletPathContext = React.createContext(null)
@@ -165,6 +169,7 @@ interface GetRoutesAndModulesFromManifestResult {
routes: Array
routeModulesToRelativePaths: RouteModulesWithRelativePaths
routingTable: RemixRoutingTable
+ customServerJSExecutor: CustomServerJSExecutor | null
}
function getRouteModulesWithPaths(
@@ -234,21 +239,17 @@ export type ExecutionScopeCreator = (
metadataContext: UiJsxCanvasContextData,
) => ExecutionScope
-function getRemixExportsOfModule(
+function createExecutionScopeCreator(
filename: string,
curriedRequireFn: CurriedUtopiaRequireFn,
curriedResolveFn: CurriedResolveFn,
- projectContents: ProjectContentTreeRoot,
-): {
- executionScopeCreator: ExecutionScopeCreator
- rootComponentUid: string
-} {
+): ExecutionScopeCreator {
let mutableContextRef: { current: MutableUtopiaCtxRefData } = { current: {} }
let topLevelComponentRendererComponents: {
current: MapLike>
} = { current: {} }
- const executionScopeCreator = (
+ return (
innerProjectContents: ProjectContentTreeRoot,
fileBlobs: CanvasBase64Blobs,
hiddenInstances: Array,
@@ -319,7 +320,22 @@ function getRemixExportsOfModule(
null,
)
}
+}
+function getRemixExportsOfModule(
+ filename: string,
+ curriedRequireFn: CurriedUtopiaRequireFn,
+ curriedResolveFn: CurriedResolveFn,
+ projectContents: ProjectContentTreeRoot,
+): {
+ executionScopeCreator: ExecutionScopeCreator
+ rootComponentUid: string
+} {
+ const executionScopeCreator = createExecutionScopeCreator(
+ filename,
+ curriedRequireFn,
+ curriedResolveFn,
+ )
const nameAndUid = getDefaultExportNameAndUidFromFile(projectContents, filename)
return {
@@ -411,11 +427,14 @@ export function getRoutesAndModulesFromManifest(
routingTable[rootComponentUid] = route.module
})
+ const customServerJSExecutor = getCustomServerJSExecutor(projectContents, curriedRequireFn)
+
return {
routeModuleCreators,
routes,
routeModulesToRelativePaths,
routingTable,
+ customServerJSExecutor,
}
}
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 295b976c98b4..5d2dd697681b 100644
--- a/editor/src/components/canvas/remix/utopia-remix-root-component.tsx
+++ b/editor/src/components/canvas/remix/utopia-remix-root-component.tsx
@@ -10,12 +10,17 @@ import * as EP from '../../../core/shared/element-path'
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 { type ExecutionScopeCreator, 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'
import { CreateRemixDerivedDataRefsGLOBAL } from '../../editor/store/remix-derived-data'
+import { type ProjectContentTreeRoot } from '../../assets'
+import { type CanvasBase64Blobs } from '../../editor/store/editor-state'
+import { type AppLoadContext } from '@remix-run/server-runtime'
+import { patchServerJSContextIntoArgs } from '../../../core/es-modules/package-manager/hydrogen-oxygen-support'
+import { patchRoutesWithContext } from '../../../third-party/remix/create-remix-stub'
type RouteModule = RouteModules[keyof RouteModules]
type RouterType = ReturnType
@@ -136,7 +141,9 @@ export const RouteExportsForRouteObject: Array = [
'shouldRevalidate',
]
-function useGetRoutes() {
+function useGetRoutes(
+ getLoadContext?: (request: Request) => Promise | AppLoadContext,
+) {
const routes = useEditorState(
Substores.derived,
(store) => store.derived.remixData?.routes ?? [],
@@ -187,9 +194,12 @@ function useGetRoutes() {
addExportsToRoutes(routes)
- return routes
+ const routesWithContext = patchRoutesWithContext(routes, getLoadContext)
+
+ return routesWithContext
}, [
displayNoneInstancesRef,
+ getLoadContext,
metadataContext,
fileBlobsRef,
hiddenInstancesRef,
@@ -201,12 +211,13 @@ function useGetRoutes() {
export interface UtopiaRemixRootComponentProps {
[UTOPIA_PATH_KEY]: ElementPath
+ getLoadContext?: (request: Request) => Promise | AppLoadContext
}
export const UtopiaRemixRootComponent = React.memo((props: UtopiaRemixRootComponentProps) => {
const remixDerivedDataRef = useRefEditorState((store) => store.derived.remixData)
- const routes = useGetRoutes()
+ const routes = useGetRoutes(props.getLoadContext)
const basePath = props[UTOPIA_PATH_KEY]
diff --git a/editor/src/components/canvas/ui-jsx-canvas-errors.spec.tsx b/editor/src/components/canvas/ui-jsx-canvas-errors.spec.tsx
index 6e683bb3a64e..aa0b003936e7 100644
--- a/editor/src/components/canvas/ui-jsx-canvas-errors.spec.tsx
+++ b/editor/src/components/canvas/ui-jsx-canvas-errors.spec.tsx
@@ -659,7 +659,7 @@ export var ${BakedInStoryboardVariableName} = (props) => {
`)
})
- it('React.useEffect at the root fails usefully', () => {
+ xit('React.useEffect at the root fails usefully', () => {
const result = testCanvasRenderInline(
null,
`import * as React from "react"
diff --git a/editor/src/components/canvas/ui-jsx-canvas-renderer/remix-scene-component.tsx b/editor/src/components/canvas/ui-jsx-canvas-renderer/remix-scene-component.tsx
index 1f6daf81dab0..537f4178eb20 100644
--- a/editor/src/components/canvas/ui-jsx-canvas-renderer/remix-scene-component.tsx
+++ b/editor/src/components/canvas/ui-jsx-canvas-renderer/remix-scene-component.tsx
@@ -7,19 +7,21 @@ import { UtopiaStyles, useColorTheme } from '../../../uuiui'
import { UTOPIA_PATH_KEY } from '../../../core/model/utopia-constants'
import * as EP from '../../../core/shared/element-path'
import { useSetAtom } from 'jotai'
+import type { AppLoadContext } from '@remix-run/server-runtime'
export const REMIX_SCENE_TESTID = 'remix-scene'
export interface RemixSceneProps {
style?: React.CSSProperties
[UTOPIA_PATH_KEY]?: string
+ getLoadContext?: (request: Request) => Promise | AppLoadContext
}
export const RemixSceneComponent = React.memo((props: React.PropsWithChildren) => {
const colorTheme = useColorTheme()
const canvasIsLive = false
- const { style, ...remainingProps } = props
+ const { style, getLoadContext, ...remainingProps } = props
const sceneStyle: React.CSSProperties = {
position: 'relative',
@@ -49,7 +51,7 @@ export const RemixSceneComponent = React.memo((props: React.PropsWithChildren
-
+
)
})
diff --git a/editor/src/components/editor/store/remix-derived-data.tsx b/editor/src/components/editor/store/remix-derived-data.tsx
index fc7b6a74fe57..9069eeff1f9a 100644
--- a/editor/src/components/editor/store/remix-derived-data.tsx
+++ b/editor/src/components/editor/store/remix-derived-data.tsx
@@ -14,6 +14,7 @@ import {
} from '../../assets'
import type { ProjectContentTreeRoot } from '../../assets'
import type {
+ ExecutionScopeCreator,
RouteIdsToModuleCreators,
RouteModulesWithRelativePaths,
} from '../../canvas/remix/remix-utils'
@@ -28,6 +29,7 @@ import type { CurriedUtopiaRequireFn, CurriedResolveFn } from '../../custom-code
import { memoize } from '../../../core/shared/memoize'
import { shallowEqual } from '../../../core/shared/equality-utils'
import { evaluator } from '../../../core/es-modules/evaluator/evaluator'
+import { type CustomServerJSExecutor } from '../../../core/es-modules/package-manager/hydrogen-oxygen-support'
export interface RemixRoutingTable {
[rootElementUid: string]: string /* file path */
@@ -40,6 +42,7 @@ export interface RemixDerivedData {
routeModulesToRelativePaths: RouteModulesWithRelativePaths
routes: Array
routingTable: RemixRoutingTable
+ customServerJSExecutor: CustomServerJSExecutor | null
}
export const CreateRemixDerivedDataRefsGLOBAL: {
@@ -112,8 +115,13 @@ export function createRemixDerivedData(
return null
}
- const { routeModuleCreators, routes, routeModulesToRelativePaths, routingTable } =
- routesAndModulesFromManifestResult
+ const {
+ routeModuleCreators,
+ routes,
+ routeModulesToRelativePaths,
+ routingTable,
+ customServerJSExecutor,
+ } = routesAndModulesFromManifestResult
return {
futureConfig: DefaultFutureConfig,
@@ -122,6 +130,7 @@ export function createRemixDerivedData(
routeModuleCreators: routeModuleCreators,
routeModulesToRelativePaths: routeModulesToRelativePaths,
routingTable: routingTable,
+ customServerJSExecutor: customServerJSExecutor,
}
}
diff --git a/editor/src/core/es-modules/evaluator/evaluator.ts b/editor/src/core/es-modules/evaluator/evaluator.ts
index 51029b0ce916..2ca4f2158191 100644
--- a/editor/src/core/es-modules/evaluator/evaluator.ts
+++ b/editor/src/core/es-modules/evaluator/evaluator.ts
@@ -111,6 +111,7 @@ export function evaluator(
const fileExtension = getFileExtension(filepath)
switch (fileExtension) {
case 'js':
+ case 'jsx':
case 'cjs':
case 'mjs':
return evaluateJs(filepath, moduleCode, fileEvaluationCache, requireFn)
diff --git a/editor/src/core/es-modules/package-manager/built-in-dependencies-list.ts b/editor/src/core/es-modules/package-manager/built-in-dependencies-list.ts
index 846cdd9e4df8..a1ebb5bb7399 100644
--- a/editor/src/core/es-modules/package-manager/built-in-dependencies-list.ts
+++ b/editor/src/core/es-modules/package-manager/built-in-dependencies-list.ts
@@ -11,6 +11,8 @@ import * as UtopiaAPI from 'utopia-api'
import * as UUIUI from '../../../uuiui'
import * as UUIUIDeps from '../../../uuiui-deps'
import * as RemixServerBuild from './built-in-third-party-dependencies/remix-server-build'
+import * as RemixOxygen from '@shopify/remix-oxygen'
+import { createRequestHandler as remixOxygenCreateRequestHandler } from './hydrogen-oxygen-support'
import { SafeLink, SafeOutlet } from './canvas-safe-remix'
import { UtopiaApiGroup } from './group-component'
@@ -120,5 +122,15 @@ export function createBuiltInDependenciesList(
builtInDependency('@remix-run/dev/server-build', RemixServerBuild, '1.19.1'),
builtInDependency('@shopify/cli', Stub, '3.49.2'),
builtInDependency('@shopify/cli-hydrogen', Stub, '5.4.2'),
+
+ // Oxygen support (hack). The request handler is required to inject context for the loaders / actions
+ builtInDependency(
+ '@shopify/remix-oxygen',
+ {
+ ...RemixOxygen,
+ createRequestHandler: remixOxygenCreateRequestHandler,
+ },
+ editorPackageJSON.dependencies['@shopify/remix-oxygen'],
+ ),
]
}
diff --git a/editor/src/core/es-modules/package-manager/hydrogen-oxygen-support.ts b/editor/src/core/es-modules/package-manager/hydrogen-oxygen-support.ts
new file mode 100644
index 000000000000..4b9c338f0ad7
--- /dev/null
+++ b/editor/src/core/es-modules/package-manager/hydrogen-oxygen-support.ts
@@ -0,0 +1,75 @@
+import { type AppLoadContext } from '@remix-run/server-runtime'
+import { type MapLike } from 'typescript'
+import { getProjectFileByFilePath, type ProjectContentTreeRoot } from '../../../components/assets'
+import { type CurriedUtopiaRequireFn } from '../../../components/custom-code/code-file'
+
+// remix-oxygen hack to create and return the context
+export function createRequestHandler({
+ getLoadContext,
+}: {
+ getLoadContext?: (request: Request) => Promise | Context
+}) {
+ return async (request: Request) => {
+ const context =
+ getLoadContext != null ? ((await getLoadContext(request)) as AppLoadContext) : undefined
+
+ const response = new Response()
+
+ return {
+ ...response,
+ context: context,
+ }
+ }
+}
+
+export type CustomServerJSExecutor = (innerProjectContents: ProjectContentTreeRoot) => MapLike
+
+export function getCustomServerJSExecutor(
+ projectContents: ProjectContentTreeRoot,
+ curriedRequireFn: CurriedUtopiaRequireFn,
+): CustomServerJSExecutor | null {
+ const serverFileName = '/server.js'
+ const hasCustomServer = getProjectFileByFilePath(projectContents, serverFileName) != null
+ if (!hasCustomServer) {
+ return null
+ }
+
+ return (innerProjectContents: ProjectContentTreeRoot) => {
+ return curriedRequireFn(innerProjectContents)('.', serverFileName, false)
+ }
+}
+
+export async function patchServerJSContextIntoArgs(
+ customServerJSExecutor: CustomServerJSExecutor | null,
+ projectContents: ProjectContentTreeRoot,
+ args: any,
+): Promise {
+ if (customServerJSExecutor == null) {
+ return args
+ }
+
+ const customServerJSScope = customServerJSExecutor(projectContents)
+
+ if (customServerJSScope.default?.fetch == null) {
+ return { context: {} }
+ }
+
+ const { context: serverJSContext } = await customServerJSScope.default.fetch(
+ args.request,
+ {
+ SESSION_SECRET: 'foobar',
+ PUBLIC_STORE_DOMAIN: 'mock.shop',
+ },
+ {
+ waitUntil: () => {},
+ },
+ )
+
+ return {
+ ...args,
+ context: {
+ ...args.context,
+ ...serverJSContext,
+ },
+ }
+}
diff --git a/editor/src/core/shared/css-utils.ts b/editor/src/core/shared/css-utils.ts
index 0c14a3d78707..8a3a9afc4e7c 100644
--- a/editor/src/core/shared/css-utils.ts
+++ b/editor/src/core/shared/css-utils.ts
@@ -1,65 +1,38 @@
import * as csstree from 'css-tree'
import { CanvasContainerID } from '../../components/canvas/canvas-types'
-const SelectorTypes = ['ClassSelector', 'IdSelector', 'TypeSelector']
-const SelectorsToSkip = [
- // general case type selectors to skip
- 'html',
- 'head',
+function scopePseudoClassSelector(): csstree.PseudoClassSelector {
+ return csstree.fromPlainObject({
+ type: 'PseudoClassSelector',
+ name: 'scope',
+ children: null,
+ }) as csstree.PseudoClassSelector
+}
- // keyframe specific type selectors
- 'from',
- 'to',
-]
+function isSelectorToChange(node: csstree.CssNode): boolean {
+ switch (node.type) {
+ case 'TypeSelector':
+ return ['body', 'html', 'head'].includes(node.name)
+ case 'PseudoClassSelector':
+ return node.name === 'root'
+ default:
+ return false
+ }
+}
export function rescopeCSSToTargetCanvasOnly(input: string): string {
- let ast = csstree.parse(input)
-
- csstree.walk(ast, (node) => {
- // We want to find all selectors, and prepend '#canvas-container ' (i.e. the canvas-container
- // ID Selector and a ' ' combinator) so that they will only apply to descendents of the canvas
- if (node.type === 'Selector') {
- const firstChild = node.children.first
-
- if (firstChild == null) {
- return
- }
-
- if (!SelectorTypes.includes(firstChild.type)) {
- return
- }
-
- if (firstChild.type === 'TypeSelector' && SelectorsToSkip.includes(firstChild.name)) {
- // Skip special selectors
- return
- }
-
- if (firstChild.type === 'TypeSelector' && firstChild.name === 'body') {
- // The closest analogy to the body here is the #canvas-container itself,
- // so let's just replace it
- node.children.shift()
- node.children.prependData(
- csstree.fromPlainObject({
- type: 'IdSelector',
- name: CanvasContainerID,
- }),
- )
- } else {
- // For everything else we want to prepent '#canvas-container '
- node.children.prependData(
- csstree.fromPlainObject({
- type: 'Combinator',
- name: ' ',
- }),
- )
-
- node.children.prependData(
- csstree.fromPlainObject({
- type: 'IdSelector',
- name: CanvasContainerID,
- }),
- )
- }
+ // First wrap it in an @scope
+ const scopedInput = `@scope (#${CanvasContainerID}) {
+ ${input}
+ }`
+
+ let ast = csstree.parse(scopedInput)
+
+ csstree.walk(ast, (node, item, list) => {
+ // As we are wrapping in an @scope, we need to redirect certain selectors to :scope
+ if (isSelectorToChange(node) && list != null) {
+ list.insertData(scopePseudoClassSelector(), item)
+ list.remove(item)
}
})
diff --git a/editor/src/third-party/remix/create-remix-stub.ts b/editor/src/third-party/remix/create-remix-stub.ts
new file mode 100644
index 000000000000..2abad97bd17f
--- /dev/null
+++ b/editor/src/third-party/remix/create-remix-stub.ts
@@ -0,0 +1,41 @@
+import type { AppLoadContext } from '@remix-run/server-runtime'
+import type { DataRouteObject, RouteObject } from 'react-router'
+
+// adopted from https://github.com/remix-run/remix/blob/4a8f558d16b8739d7f70903958ac5792e050f3e9/packages/remix-testing/create-remix-stub.tsx#L55
+// replaced the types with the vanilla types from react-router
+export function patchRoutesWithContext(
+ routes: Array,
+ getLoadContext?: (request: Request) => Promise | AppLoadContext, // the async getLoadContext has been lifted from the server adapter https://remix.run/docs/en/main/route/loader#context
+): (RouteObject | DataRouteObject)[] {
+ if (getLoadContext == null) {
+ // no context to patch with
+ return routes
+ }
+
+ return routes.map((route) => {
+ if (route.loader) {
+ let loader = route.loader
+ route.loader = async (args) => {
+ const context = await getLoadContext(args.request)
+ return loader({ ...args, context: context })
+ }
+ }
+
+ if (route.action) {
+ let action = route.action
+ route.action = async (args) => {
+ const context = await getLoadContext(args.request)
+ return action({ ...args, context: context })
+ }
+ }
+
+ if (route.children) {
+ return {
+ ...route,
+ children: patchRoutesWithContext(route.children, getLoadContext),
+ }
+ }
+
+ return route as RouteObject | DataRouteObject
+ }) as (RouteObject | DataRouteObject)[]
+}