Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 14 additions & 0 deletions editor/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
<Storyboard>
<RemixScene
style={{
width: 700,
height: 759,
position: 'absolute',
left: 212,
top: 128,
}}
data-label='Playground'
getLoadContext={getLoadContext}
/>
</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 (
<div>
Request given to loader matches the request given to getLoadContext: {JSON.stringify(loaderData.contextEqualsRequest)}
<div>${RootTextContent}</div>
<Outlet />
</div>
)
}
`,
['/app/routes/_index.js']: `import React from 'react'

const Index = () => (<h1>${DefaultRouteTextContent}</h1>)
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({
Expand Down
33 changes: 26 additions & 7 deletions editor/src/components/canvas/remix/remix-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ElementPath | null>(null)

Expand Down Expand Up @@ -165,6 +169,7 @@ interface GetRoutesAndModulesFromManifestResult {
routes: Array<DataRouteObject>
routeModulesToRelativePaths: RouteModulesWithRelativePaths
routingTable: RemixRoutingTable
customServerJSExecutor: CustomServerJSExecutor | null
}

function getRouteModulesWithPaths(
Expand Down Expand Up @@ -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<MapLike<ComponentRendererComponent>>
} = { current: {} }

const executionScopeCreator = (
return (
innerProjectContents: ProjectContentTreeRoot,
fileBlobs: CanvasBase64Blobs,
hiddenInstances: Array<ElementPath>,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -411,11 +427,14 @@ export function getRoutesAndModulesFromManifest(
routingTable[rootComponentUid] = route.module
})

const customServerJSExecutor = getCustomServerJSExecutor(projectContents, curriedRequireFn)

return {
routeModuleCreators,
routes,
routeModulesToRelativePaths,
routingTable,
customServerJSExecutor,
}
}

Expand Down
19 changes: 15 additions & 4 deletions editor/src/components/canvas/remix/utopia-remix-root-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof createMemoryRouter>
Expand Down Expand Up @@ -136,7 +141,9 @@ export const RouteExportsForRouteObject: Array<keyof RouteObject> = [
'shouldRevalidate',
]

function useGetRoutes() {
function useGetRoutes(
getLoadContext?: (request: Request) => Promise<AppLoadContext> | AppLoadContext,
) {
const routes = useEditorState(
Substores.derived,
(store) => store.derived.remixData?.routes ?? [],
Expand Down Expand Up @@ -187,9 +194,12 @@ function useGetRoutes() {

addExportsToRoutes(routes)

return routes
const routesWithContext = patchRoutesWithContext(routes, getLoadContext)

return routesWithContext
}, [
displayNoneInstancesRef,
getLoadContext,
metadataContext,
fileBlobsRef,
hiddenInstancesRef,
Expand All @@ -201,12 +211,13 @@ function useGetRoutes() {

export interface UtopiaRemixRootComponentProps {
[UTOPIA_PATH_KEY]: ElementPath
getLoadContext?: (request: Request) => Promise<AppLoadContext> | 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]

Expand Down
2 changes: 1 addition & 1 deletion editor/src/components/canvas/ui-jsx-canvas-errors.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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> | AppLoadContext
}

export const RemixSceneComponent = React.memo((props: React.PropsWithChildren<RemixSceneProps>) => {
const colorTheme = useColorTheme()
const canvasIsLive = false

const { style, ...remainingProps } = props
const { style, getLoadContext, ...remainingProps } = props

const sceneStyle: React.CSSProperties = {
position: 'relative',
Expand Down Expand Up @@ -49,7 +51,7 @@ export const RemixSceneComponent = React.memo((props: React.PropsWithChildren<Re
style={sceneStyle}
onMouseDown={onMouseDown}
>
<UtopiaRemixRootComponent data-path={path} />
<UtopiaRemixRootComponent data-path={path} getLoadContext={getLoadContext} />
</div>
)
})
13 changes: 11 additions & 2 deletions editor/src/components/editor/store/remix-derived-data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from '../../assets'
import type { ProjectContentTreeRoot } from '../../assets'
import type {
ExecutionScopeCreator,
RouteIdsToModuleCreators,
RouteModulesWithRelativePaths,
} from '../../canvas/remix/remix-utils'
Expand All @@ -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 */
Expand All @@ -40,6 +42,7 @@ export interface RemixDerivedData {
routeModulesToRelativePaths: RouteModulesWithRelativePaths
routes: Array<DataRouteObject>
routingTable: RemixRoutingTable
customServerJSExecutor: CustomServerJSExecutor | null
}

export const CreateRemixDerivedDataRefsGLOBAL: {
Expand Down Expand Up @@ -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,
Expand All @@ -122,6 +130,7 @@ export function createRemixDerivedData(
routeModuleCreators: routeModuleCreators,
routeModulesToRelativePaths: routeModulesToRelativePaths,
routingTable: routingTable,
customServerJSExecutor: customServerJSExecutor,
}
}

Expand Down
1 change: 1 addition & 0 deletions editor/src/core/es-modules/evaluator/evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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'],
),
]
}
Loading