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
2 changes: 2 additions & 0 deletions editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@
"@emotion/styled": "11.0.0",
"@emotion/styled-base": "11.0.0",
"@popperjs/core": "2.4.4",
"@shopify/remix-oxygen": "1.1.7",
"@remix-run/react": "2.0.1",
"@remix-run/server-runtime": "2.0.1",
"@root/encoding": "1.0.1",
"@seznam/compose-react-refs": "^1.0.4",
"@stitches/react": "1.2.8",
Expand Down
32 changes: 31 additions & 1 deletion editor/pnpm-lock.yaml

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

2 changes: 2 additions & 0 deletions editor/src/components/canvas/remix/remix-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,8 @@ export function getRoutesAndModulesFromManifest(
const routeModuleCreators: RouteIdsToModuleCreators = {}
const routingTable: RemixRoutingTable = {}

// The root directory should come from remix.config.js, and should default to `/app`, not `/src`
// The root file should be root.jsx
const rootJsFile = getProjectFileByFilePath(projectContents, `${ROOT_DIR}/root.js`)
if (rootJsFile == null || rootJsFile.type !== 'TEXT_FILE') {
return null
Expand Down
126 changes: 85 additions & 41 deletions editor/src/components/canvas/remix/utopia-remix-root-component.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RemixContext } from '@remix-run/react/dist/components'
import { UNSAFE_RemixContext as RemixContext } from '@remix-run/react'
import type { RouteModules, RouteModule } from '@remix-run/react/dist/routeModules'
import React from 'react'
import type { DataRouteObject, Location } from 'react-router'
Expand All @@ -8,7 +8,7 @@ import type { ElementPath } from '../../../core/shared/project-file-types'
import { useRefEditorState, useEditorState, Substores } from '../../editor/store/store-hook'
import * as EP from '../../../core/shared/element-path'
import { PathPropHOC } from './path-props-hoc'
import { atom, useAtom, useSetAtom } from 'jotai'
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
import { getDefaultExportNameAndUidFromFile } from '../../../core/model/project-file-utils'
import { OutletPathContext } from './remix-utils'
import { UiJsxCanvasCtxAtom } from '../ui-jsx-canvas'
Expand Down Expand Up @@ -76,20 +76,22 @@ function useGetRouteModules(basePath: ElementPath) {
continue
}

const createExecutionScope = () =>
value.executionScopeCreator(
projectContentsRef.current,
fileBlobsRef.current,
hiddenInstancesRef.current,
displayNoneInstancesRef.current,
metadataContext,
).scope
// We only want to call this once, when mounting the UtopiaRemixRootComponent, as otherwise
// any time we render the default export we'll actually be remounting everything
const executionScope = value.executionScopeCreator(
projectContentsRef.current,
fileBlobsRef.current,
hiddenInstancesRef.current,
displayNoneInstancesRef.current,
metadataContext,
).scope

// TODO Balazs wants us to actually make this look like a component
const defaultComponent = (componentProps: any) =>
createExecutionScope()[nameAndUid.name]?.(componentProps) ?? <React.Fragment />
executionScope[nameAndUid.name]?.(componentProps) ?? <React.Fragment />

const errorBoundary = value.createErrorBoundary
? (componentProps: any) => createExecutionScope()['ErrorBoundary']?.(componentProps) ?? null
? (componentProps: any) => executionScope['ErrorBoundary']?.(componentProps) ?? null
: undefined

const routeModule: RouteModule = {
Expand Down Expand Up @@ -141,30 +143,65 @@ function useGetRoutes() {

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]
const creatorForRoute = creators[route.id]
const executionScope = creatorForRoute.executionScopeCreator(
projectContentsRef.current,
fileBlobsRef.current,
hiddenInstancesRef.current,
displayNoneInstancesRef.current,
metadataContext,
).scope

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
route.action = async (args: any) => {
// Super hacky way of calling the `fetch` function from `server.js` purely to get the context
// that it provides, so that we can then provide that to the action function
const { context: actionContext } = await executionScope['customServer'].fetch(
args.request,
{
SESSION_SECRET: 'foobar',
PUBLIC_STORE_DOMAIN: 'mock.shop',
},
{
waitUntil: () => {},
},
)

const patchedArgs = {
...args,
context: {
...args.context,
...actionContext,
},
}

return (await executionScope['action']?.(patchedArgs)) ?? null
}

route.loader = async (args: any) => {
// Super hacky way of calling the `fetch` function from `server.js` purely to get the context
// that it provides, so that we can then provide that to the loader function
const { context: loaderContext } = await executionScope['customServer'].fetch(
args.request,
{
SESSION_SECRET: 'foobar',
PUBLIC_STORE_DOMAIN: 'mock.shop',
},
{
waitUntil: () => {},
},
)

const patchedArgs = {
...args,
context: {
...args.context,
...loaderContext,
},
}

return (await executionScope['loader']?.(patchedArgs)) ?? null
}
}

addLoaderAndActionToRoutes(route.children ?? [])
Expand All @@ -189,6 +226,16 @@ export interface UtopiaRemixRootComponentProps {
[UTOPIA_PATH_KEY]: ElementPath
}

function useCurrentEntriesRef(basePath: string) {
const navigationData = useAtomValue(RemixNavigationAtom)

const currentEntries = navigationData[basePath]?.entries
const currentEntriesRef = React.useRef(currentEntries)
currentEntriesRef.current = currentEntries

return currentEntriesRef
}

export const UtopiaRemixRootComponent = React.memo((props: UtopiaRemixRootComponentProps) => {
const remixDerivedDataRef = useRefEditorState((store) => store.derived.remixData)

Expand All @@ -198,11 +245,8 @@ export const UtopiaRemixRootComponent = React.memo((props: UtopiaRemixRootCompon

const routeModules = useGetRouteModules(basePath)

const [navigationData, setNavigationData] = useAtom(RemixNavigationAtom)

const currentEntries = navigationData[EP.toString(basePath)]?.entries
const currentEntriesRef = React.useRef(currentEntries)
currentEntriesRef.current = currentEntries
const setNavigationData = useSetAtom(RemixNavigationAtom)
const currentEntriesRef = useCurrentEntriesRef(EP.toString(basePath))

// The router always needs to be updated otherwise new routes won't work without a refresh
// We need to create the new router with the current location in the initial entries to
Expand All @@ -213,7 +257,7 @@ export const UtopiaRemixRootComponent = React.memo((props: UtopiaRemixRootCompon
}
const initialEntries = currentEntriesRef.current == null ? undefined : currentEntriesRef.current
return createMemoryRouter(routes, { initialEntries: initialEntries })
}, [routes])
}, [routes, currentEntriesRef])

const setNavigationDataForRouter = React.useCallback(
(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,26 @@ export function createExecutionScope(

const fileBlobsForFile = defaultIfNull(emptyFileBlobs, fileBlobs[filePath])

const { topLevelElements, imports, jsxFactoryFunction, combinedTopLevelArbitraryBlock } =
getParseSuccessForFilePath(filePath, projectContents)
const {
topLevelElements,
imports: rawImports,
jsxFactoryFunction,
combinedTopLevelArbitraryBlock,
} = getParseSuccessForFilePath(filePath, projectContents)

// HACK Add the custom server file to the imports here
const isRouteFile = filePath === '/src/root.js' || filePath.startsWith('/src/routes/')
const imports = isRouteFile
? {
...rawImports,
'/server': {
importedAs: null,
importedFromWithin: [],
importedWithName: 'customServer',
},
}
: rawImports

const requireResult: MapLike<any> = importResultFromImports(filePath, imports, customRequire)

const userRequireFn = (toImport: string) => customRequire(filePath, toImport) // TODO this was a React usecallback
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@ import React from 'react' // this is imported like this so that the monkey patch
import * as ReactDOM from 'react-dom'
import * as EmotionReact from '@emotion/react'
import * as EmotionStyled from '@emotion/styled'
import * as RemixOxygen from '@shopify/remix-oxygen'

import editorPackageJSON from '../../../../package.json'
import utopiaAPIPackageJSON from '../../../../../utopia-api/package.json'
import { applyUIDMonkeyPatch } from '../../../utils/canvas-react-utils'
import { createRegisterModuleFunction } from '../../property-controls/property-controls-local'
import type { EditorDispatch } from '../../../components/editor/action-types'
import type { EditorState } from '../../../components/editor/store/editor-state'
import type { UtopiaTsWorkers } from '../../workers/common/worker-types'
import { UtopiaApiGroup } from './group-component'
import * as RemixRunReact from '@remix-run/react'
import * as ReactRouter from 'react-router'
import { SafeLink, SafeOutlet } from './canvas-safe-remix'
import { createRequestHandler as remixOxygenCreateRequestHandler } from './remix-oxygen'
import * as RemixServerBuild from './remix-server-build'

applyUIDMonkeyPatch()

Expand Down Expand Up @@ -88,6 +89,15 @@ export function createBuiltInDependenciesList(
EmotionStyled,
editorPackageJSON.dependencies['@emotion/styled'],
),
builtInDependency(
'@shopify/remix-oxygen',
{
...RemixOxygen,
createRequestHandler: remixOxygenCreateRequestHandler,
},
editorPackageJSON.dependencies['@shopify/remix-oxygen'],
),
builtInDependency('@remix-run/dev/server-build', RemixServerBuild, '1.19.1'),
builtInDependency(
'@remix-run/react',
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -500,8 +500,10 @@ export function resolveModule(
projectContents: ProjectContentTreeRoot,
nodeModules: NodeModules,
importOrigin: string,
toImport: string,
toImportRaw: string,
): FileLookupResult {
// This is because an alias is provided in jsconfig.json
const toImport = toImportRaw.startsWith('~') ? `/src/${toImportRaw.slice(1)}` : toImportRaw
return resolveModuleAndApplySubstitutions(projectContents, nodeModules, importOrigin, toImport)
}

Expand Down
18 changes: 18 additions & 0 deletions editor/src/core/es-modules/package-manager/remix-oxygen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { type AppLoadContext } from '@remix-run/server-runtime'

export function createRequestHandler<Context = unknown>({
getLoadContext,
}: {
getLoadContext?: (request: Request) => Promise<Context> | Context
}) {
return async (request: Request) => {
const context =
getLoadContext != null ? ((await getLoadContext(request)) as AppLoadContext) : undefined

// This would maybe have to encode the context in the body and return a real Response
return {
status: 200,
context: context,
}
}
}
10 changes: 10 additions & 0 deletions editor/src/core/es-modules/package-manager/remix-server-build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { ServerBuild } from '@remix-run/server-runtime'

export declare const assets: ServerBuild['assets']
export declare const entry: ServerBuild['entry']
export declare const routes: ServerBuild['routes']
export declare const future: ServerBuild['future']
export declare const publicPath: ServerBuild['publicPath']
export declare const assetsBuildDirectory: ServerBuild['assetsBuildDirectory']

// We don't really need these, but otherwise the import in server.js will break
Loading