From 0896f87e1b342a0aae7c0fe83769aad9f462c2b5 Mon Sep 17 00:00:00 2001 From: RheeseyB <1044774+Rheeseyb@users.noreply.github.com> Date: Fri, 24 Nov 2023 11:45:49 +0000 Subject: [PATCH 1/7] spike(editor) Call the server.js's fetch function when calling server functions --- editor/package.json | 1 + editor/pnpm-lock.yaml | 14 +++ .../components/canvas/remix/remix-utils.tsx | 41 ++++++--- .../remix/utopia-remix-root-component.tsx | 88 ++++++++++++++++--- .../ui-jsx-canvas-execution-scope.tsx | 26 +++++- .../editor/store/remix-derived-data.tsx | 12 ++- .../core/es-modules/evaluator/evaluator.ts | 1 + .../built-in-dependencies-list.ts | 12 +++ .../package-manager/remix-oxygen.ts | 19 ++++ 9 files changed, 186 insertions(+), 28 deletions(-) create mode 100644 editor/src/core/es-modules/package-manager/remix-oxygen.ts diff --git a/editor/package.json b/editor/package.json index 34c1e82d142d..e5c2a5ab0389 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 83efbdcf8848..b04187790b13 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-utils.tsx b/editor/src/components/canvas/remix/remix-utils.tsx index 49462aa8ff28..e73c1499cc64 100644 --- a/editor/src/components/canvas/remix/remix-utils.tsx +++ b/editor/src/components/canvas/remix/remix-utils.tsx @@ -165,6 +165,7 @@ interface GetRoutesAndModulesFromManifestResult { routes: Array routeModulesToRelativePaths: RouteModulesWithRelativePaths routingTable: RemixRoutingTable + customServerCreator: ExecutionScopeCreator | null } function getRouteModulesWithPaths( @@ -234,28 +235,24 @@ 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, displayNoneInstances: Array, metadataContext: UiJsxCanvasContextData, ) => { - let resolvedFiles: MapLike> = {} + let resolvedFiles: MapLike> = {} let resolvedFileNames: Array = [filename] const requireFn = curriedRequireFn(innerProjectContents) @@ -267,11 +264,12 @@ function getRemixExportsOfModule( } let resolvedFromThisOrigin = resolvedFiles[importOrigin] - const alreadyResolved = resolvedFromThisOrigin.includes(toImport) // We're inside a cyclic dependency, so trigger the below fallback const filePathResolveResult = alreadyResolved + const alreadyResolved = resolvedFromThisOrigin[toImport] !== undefined + const filePathResolveResult = alreadyResolved ? left('Already resolved') : resolve(importOrigin, toImport) - forEachRight(alreadyResolved, (filepath) => resolvedFileNames.push(filepath)) + forEachRight(filePathResolveResult, (filepath) => resolvedFileNames.push(filepath)) const resolvedParseSuccess: Either> = attemptToResolveParsedComponents( resolvedFromThisOrigin, @@ -287,7 +285,7 @@ function getRemixExportsOfModule( metadataContext, NO_OP, false, - alreadyResolved, + filePathResolveResult, null, ) return foldEither( @@ -318,7 +316,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 { @@ -410,11 +423,17 @@ export function getRoutesAndModulesFromManifest( routingTable[rootComponentUid] = route.module }) + const hasCustomServer = getProjectFileByFilePath(projectContents, '/server.js') != null + const customServerCreator: ExecutionScopeCreator | null = hasCustomServer + ? createExecutionScopeCreator('/server.js', curriedRequireFn, curriedResolveFn) + : null + return { routeModuleCreators, routes, routeModulesToRelativePaths, routingTable, + customServerCreator, } } 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 a9fadca98343..3aa7058a9358 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,15 @@ 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' type RouteModule = RouteModules[keyof RouteModules] type RouterType = ReturnType @@ -150,25 +153,45 @@ function useGetRoutes() { return routes } + const customServerCreator = remixDerivedDataRef.current.customServerCreator const creators = remixDerivedDataRef.current.routeModuleCreators function addExportsToRoutes(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 = creators[route.id] ?? null if (creatorForRoute != null) { for (const routeExport of RouteExportsForRouteObject) { - route[routeExport] = (args: any) => - creatorForRoute - .executionScopeCreator( - projectContentsRef.current, - fileBlobsRef.current, - hiddenInstancesRef.current, - displayNoneInstancesRef.current, - metadataContext, - ) - .scope[routeExport]?.(args) ?? null + route[routeExport] = async (args: any) => { + const { context: requestContext } = await getContextFromCustomServer( + customServerCreator, + projectContentsRef.current, + fileBlobsRef.current, + hiddenInstancesRef.current, + displayNoneInstancesRef.current, + metadataContext, + args.request, + ) + + const patchedArgs = { + ...args, + context: { + ...args.context, + ...requestContext, + }, + } + + return ( + creatorForRoute + .executionScopeCreator( + projectContentsRef.current, + fileBlobsRef.current, + hiddenInstancesRef.current, + displayNoneInstancesRef.current, + metadataContext, + ) + .scope[routeExport]?.(patchedArgs) ?? null + ) + } } } @@ -190,6 +213,45 @@ function useGetRoutes() { ]) } +// 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 server function +async function getContextFromCustomServer( + customServerCreator: ExecutionScopeCreator | null, + projectContents: ProjectContentTreeRoot, + fileBlobs: CanvasBase64Blobs, + hiddenInstances: Array, + displayNoneInstances: Array, + metadataContext: UiJsxCanvasContextData, + request: Request, +): Promise<{ context: AppLoadContext }> { + if (customServerCreator == null) { + return { context: {} } + } + + const customServerScope = customServerCreator( + projectContents, + fileBlobs, + hiddenInstances, + displayNoneInstances, + metadataContext, + ).scope + + if (customServerScope.default?.fetch == null) { + return { context: {} } + } + + return customServerScope.default.fetch( + request, + { + SESSION_SECRET: 'foobar', + PUBLIC_STORE_DOMAIN: 'mock.shop', + }, + { + waitUntil: () => {}, + }, + ) +} + export interface UtopiaRemixRootComponentProps { [UTOPIA_PATH_KEY]: ElementPath } diff --git a/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-execution-scope.tsx b/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-execution-scope.tsx index c97458367db2..8c0678957d86 100644 --- a/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-execution-scope.tsx +++ b/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-execution-scope.tsx @@ -67,8 +67,30 @@ export function createExecutionScope( const fileBlobsForFile = defaultIfNull(emptyFileBlobs, fileBlobs[filePath]) - const { topLevelElements, imports, jsxFactoryFunction, combinedTopLevelArbitraryBlock } = - getParseSuccessForFilePath(filePath, projectContents) + const projectFile = getProjectFileByFilePath(projectContents, filePath) + if (projectFile == null || !isTextFile(projectFile)) { + return { + scope: {}, + topLevelJsxComponents: new Map(), + requireResult: {}, + } + } + + // If this is a parse failure we should still execute it + if (!isParseSuccess(projectFile.fileContents.parsed)) { + const scope = customRequire('.', filePath) + return { + scope: scope, + topLevelJsxComponents: new Map(), + requireResult: {}, + } + } + + const parseSuccess = projectFile.fileContents.parsed + const topLevelElements = parseSuccess.topLevelElements + const imports = parseSuccess.imports + const jsxFactoryFunction = parseSuccess.jsxFactoryFunction + const combinedTopLevelArbitraryBlock = parseSuccess.combinedTopLevelArbitraryBlock const requireResult: MapLike = importResultFromImports(filePath, imports, customRequire) const userRequireFn = (toImport: string) => customRequire(filePath, toImport) // TODO this was a React usecallback diff --git a/editor/src/components/editor/store/remix-derived-data.tsx b/editor/src/components/editor/store/remix-derived-data.tsx index fc7b6a74fe57..af5ce204d18d 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' @@ -40,6 +41,7 @@ export interface RemixDerivedData { routeModulesToRelativePaths: RouteModulesWithRelativePaths routes: Array routingTable: RemixRoutingTable + customServerCreator: ExecutionScopeCreator | null } export const CreateRemixDerivedDataRefsGLOBAL: { @@ -112,8 +114,13 @@ export function createRemixDerivedData( return null } - const { routeModuleCreators, routes, routeModulesToRelativePaths, routingTable } = - routesAndModulesFromManifestResult + const { + routeModuleCreators, + routes, + routeModulesToRelativePaths, + routingTable, + customServerCreator, + } = routesAndModulesFromManifestResult return { futureConfig: DefaultFutureConfig, @@ -122,6 +129,7 @@ export function createRemixDerivedData( routeModuleCreators: routeModuleCreators, routeModulesToRelativePaths: routeModulesToRelativePaths, routingTable: routingTable, + customServerCreator: customServerCreator, } } 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..d9b4fac678a2 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 './remix-oxygen' 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/remix-oxygen.ts b/editor/src/core/es-modules/package-manager/remix-oxygen.ts new file mode 100644 index 000000000000..d65e8e31a378 --- /dev/null +++ b/editor/src/core/es-modules/package-manager/remix-oxygen.ts @@ -0,0 +1,19 @@ +import { type AppLoadContext } from '@remix-run/server-runtime' + +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, + } + } +} From 7d320e47fa87cb584aac2ed6c684fbfeb8689cd3 Mon Sep 17 00:00:00 2001 From: RheeseyB <1044774+Rheeseyb@users.noreply.github.com> Date: Fri, 24 Nov 2023 15:26:12 +0000 Subject: [PATCH 2/7] chore(editor) Pull out all of the spike code into a sinlge unapologetic file --- .../components/canvas/remix/remix-utils.tsx | 13 ++-- .../remix/utopia-remix-root-component.tsx | 21 ++---- .../canvas/ui-jsx-canvas-errors.spec.tsx | 2 +- .../ui-jsx-canvas-execution-scope.tsx | 26 +------ .../editor/store/remix-derived-data.tsx | 7 +- .../built-in-dependencies-list.ts | 2 +- .../hydrogen-oxygen-support.ts | 75 +++++++++++++++++++ .../package-manager/remix-oxygen.ts | 19 ----- 8 files changed, 95 insertions(+), 70 deletions(-) create mode 100644 editor/src/core/es-modules/package-manager/hydrogen-oxygen-support.ts delete mode 100644 editor/src/core/es-modules/package-manager/remix-oxygen.ts diff --git a/editor/src/components/canvas/remix/remix-utils.tsx b/editor/src/components/canvas/remix/remix-utils.tsx index e73c1499cc64..08073512e2dc 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,7 +169,7 @@ interface GetRoutesAndModulesFromManifestResult { routes: Array routeModulesToRelativePaths: RouteModulesWithRelativePaths routingTable: RemixRoutingTable - customServerCreator: ExecutionScopeCreator | null + customServerJSExecutor: CustomServerJSExecutor | null } function getRouteModulesWithPaths( @@ -423,17 +427,14 @@ export function getRoutesAndModulesFromManifest( routingTable[rootComponentUid] = route.module }) - const hasCustomServer = getProjectFileByFilePath(projectContents, '/server.js') != null - const customServerCreator: ExecutionScopeCreator | null = hasCustomServer - ? createExecutionScopeCreator('/server.js', curriedRequireFn, curriedResolveFn) - : null + const customServerJSExecutor = getCustomServerJSExecutor(projectContents, curriedRequireFn) return { routeModuleCreators, routes, routeModulesToRelativePaths, routingTable, - customServerCreator, + 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 3aa7058a9358..21c9f89ade67 100644 --- a/editor/src/components/canvas/remix/utopia-remix-root-component.tsx +++ b/editor/src/components/canvas/remix/utopia-remix-root-component.tsx @@ -19,6 +19,7 @@ import { CreateRemixDerivedDataRefsGLOBAL } from '../../editor/store/remix-deriv 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' type RouteModule = RouteModules[keyof RouteModules] type RouterType = ReturnType @@ -153,7 +154,7 @@ function useGetRoutes() { return routes } - const customServerCreator = remixDerivedDataRef.current.customServerCreator + const customServerJSExecutor = remixDerivedDataRef.current.customServerJSExecutor const creators = remixDerivedDataRef.current.routeModuleCreators function addExportsToRoutes(innerRoutes: DataRouteObject[]) { @@ -162,24 +163,12 @@ function useGetRoutes() { if (creatorForRoute != null) { for (const routeExport of RouteExportsForRouteObject) { route[routeExport] = async (args: any) => { - const { context: requestContext } = await getContextFromCustomServer( - customServerCreator, + const patchedArgs = await patchServerJSContextIntoArgs( + customServerJSExecutor, projectContentsRef.current, - fileBlobsRef.current, - hiddenInstancesRef.current, - displayNoneInstancesRef.current, - metadataContext, - args.request, + args, ) - const patchedArgs = { - ...args, - context: { - ...args.context, - ...requestContext, - }, - } - return ( creatorForRoute .executionScopeCreator( 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/ui-jsx-canvas-execution-scope.tsx b/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-execution-scope.tsx index 8c0678957d86..c97458367db2 100644 --- a/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-execution-scope.tsx +++ b/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-execution-scope.tsx @@ -67,30 +67,8 @@ export function createExecutionScope( const fileBlobsForFile = defaultIfNull(emptyFileBlobs, fileBlobs[filePath]) - const projectFile = getProjectFileByFilePath(projectContents, filePath) - if (projectFile == null || !isTextFile(projectFile)) { - return { - scope: {}, - topLevelJsxComponents: new Map(), - requireResult: {}, - } - } - - // If this is a parse failure we should still execute it - if (!isParseSuccess(projectFile.fileContents.parsed)) { - const scope = customRequire('.', filePath) - return { - scope: scope, - topLevelJsxComponents: new Map(), - requireResult: {}, - } - } - - const parseSuccess = projectFile.fileContents.parsed - const topLevelElements = parseSuccess.topLevelElements - const imports = parseSuccess.imports - const jsxFactoryFunction = parseSuccess.jsxFactoryFunction - const combinedTopLevelArbitraryBlock = parseSuccess.combinedTopLevelArbitraryBlock + const { topLevelElements, imports, jsxFactoryFunction, combinedTopLevelArbitraryBlock } = + getParseSuccessForFilePath(filePath, projectContents) const requireResult: MapLike = importResultFromImports(filePath, imports, customRequire) const userRequireFn = (toImport: string) => customRequire(filePath, toImport) // TODO this was a React usecallback diff --git a/editor/src/components/editor/store/remix-derived-data.tsx b/editor/src/components/editor/store/remix-derived-data.tsx index af5ce204d18d..9069eeff1f9a 100644 --- a/editor/src/components/editor/store/remix-derived-data.tsx +++ b/editor/src/components/editor/store/remix-derived-data.tsx @@ -29,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 */ @@ -41,7 +42,7 @@ export interface RemixDerivedData { routeModulesToRelativePaths: RouteModulesWithRelativePaths routes: Array routingTable: RemixRoutingTable - customServerCreator: ExecutionScopeCreator | null + customServerJSExecutor: CustomServerJSExecutor | null } export const CreateRemixDerivedDataRefsGLOBAL: { @@ -119,7 +120,7 @@ export function createRemixDerivedData( routes, routeModulesToRelativePaths, routingTable, - customServerCreator, + customServerJSExecutor, } = routesAndModulesFromManifestResult return { @@ -129,7 +130,7 @@ export function createRemixDerivedData( routeModuleCreators: routeModuleCreators, routeModulesToRelativePaths: routeModulesToRelativePaths, routingTable: routingTable, - customServerCreator: customServerCreator, + customServerJSExecutor: customServerJSExecutor, } } 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 d9b4fac678a2..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 @@ -12,7 +12,7 @@ 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 './remix-oxygen' +import { createRequestHandler as remixOxygenCreateRequestHandler } from './hydrogen-oxygen-support' import { SafeLink, SafeOutlet } from './canvas-safe-remix' import { UtopiaApiGroup } from './group-component' 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/es-modules/package-manager/remix-oxygen.ts b/editor/src/core/es-modules/package-manager/remix-oxygen.ts deleted file mode 100644 index d65e8e31a378..000000000000 --- a/editor/src/core/es-modules/package-manager/remix-oxygen.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { type AppLoadContext } from '@remix-run/server-runtime' - -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, - } - } -} From 59d9c3da8da3efa2fee5d8ce025032247fe31565 Mon Sep 17 00:00:00 2001 From: RheeseyB <1044774+Rheeseyb@users.noreply.github.com> Date: Thu, 7 Dec 2023 10:41:08 +0000 Subject: [PATCH 3/7] cherry picked CSS at scope change --- editor/src/core/shared/css-utils.ts | 85 ++++++++++------------------- 1 file changed, 29 insertions(+), 56 deletions(-) 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) } }) From e9ddd1a6597db9f93cda5271ded9281165f32e1b Mon Sep 17 00:00:00 2001 From: Balazs Bajorics <2226774+balazsbajorics@users.noreply.github.com> Date: Mon, 11 Dec 2023 14:00:49 +0100 Subject: [PATCH 4/7] removing unused code --- .../remix/utopia-remix-root-component.tsx | 39 ------------------- 1 file changed, 39 deletions(-) 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 71426f4e7c1c..00e667a11413 100644 --- a/editor/src/components/canvas/remix/utopia-remix-root-component.tsx +++ b/editor/src/components/canvas/remix/utopia-remix-root-component.tsx @@ -211,45 +211,6 @@ function useGetRoutes() { ]) } -// 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 server function -async function getContextFromCustomServer( - customServerCreator: ExecutionScopeCreator | null, - projectContents: ProjectContentTreeRoot, - fileBlobs: CanvasBase64Blobs, - hiddenInstances: Array, - displayNoneInstances: Array, - metadataContext: UiJsxCanvasContextData, - request: Request, -): Promise<{ context: AppLoadContext }> { - if (customServerCreator == null) { - return { context: {} } - } - - const customServerScope = customServerCreator( - projectContents, - fileBlobs, - hiddenInstances, - displayNoneInstances, - metadataContext, - ).scope - - if (customServerScope.default?.fetch == null) { - return { context: {} } - } - - return customServerScope.default.fetch( - request, - { - SESSION_SECRET: 'foobar', - PUBLIC_STORE_DOMAIN: 'mock.shop', - }, - { - waitUntil: () => {}, - }, - ) -} - export interface UtopiaRemixRootComponentProps { [UTOPIA_PATH_KEY]: ElementPath } From 13027c42cc8cd1a0b75cf14fda0a8fe8b2966207 Mon Sep 17 00:00:00 2001 From: Balazs Bajorics <2226774+balazsbajorics@users.noreply.github.com> Date: Mon, 11 Dec 2023 21:53:26 +0100 Subject: [PATCH 5/7] RemixScene gets a getLoadContext, removing hydrogen-specific weirdness from utopia-remix-root-component --- .../remix/utopia-remix-root-component.tsx | 45 +++++++++---------- .../remix-scene-component.tsx | 4 +- .../third-party/remix/create-remix-stub.ts | 41 +++++++++++++++++ 3 files changed, 66 insertions(+), 24 deletions(-) create mode 100644 editor/src/third-party/remix/create-remix-stub.ts 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 00e667a11413..5d2dd697681b 100644 --- a/editor/src/components/canvas/remix/utopia-remix-root-component.tsx +++ b/editor/src/components/canvas/remix/utopia-remix-root-component.tsx @@ -20,6 +20,7 @@ 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 @@ -140,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 ?? [], @@ -163,33 +166,25 @@ function useGetRoutes() { return routes } - const customServerJSExecutor = remixDerivedDataRef.current.customServerJSExecutor const creators = remixDerivedDataRef.current.routeModuleCreators function addExportsToRoutes(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 = creators[route.id] ?? null if (creatorForRoute != null) { for (const routeExport of RouteExportsForRouteObject) { - route[routeExport] = async (args: any) => { - const patchedArgs = await patchServerJSContextIntoArgs( - customServerJSExecutor, - projectContentsRef.current, - args, - ) - - return ( - creatorForRoute - .executionScopeCreator( - projectContentsRef.current, - fileBlobsRef.current, - hiddenInstancesRef.current, - displayNoneInstancesRef.current, - metadataContext, - ) - .scope[routeExport]?.(patchedArgs) ?? null - ) - } + route[routeExport] = (args: any) => + creatorForRoute + .executionScopeCreator( + projectContentsRef.current, + fileBlobsRef.current, + hiddenInstancesRef.current, + displayNoneInstancesRef.current, + metadataContext, + ) + .scope[routeExport]?.(args) ?? null } } @@ -199,9 +194,12 @@ function useGetRoutes() { addExportsToRoutes(routes) - return routes + const routesWithContext = patchRoutesWithContext(routes, getLoadContext) + + return routesWithContext }, [ displayNoneInstancesRef, + getLoadContext, metadataContext, fileBlobsRef, hiddenInstancesRef, @@ -213,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-renderer/remix-scene-component.tsx b/editor/src/components/canvas/ui-jsx-canvas-renderer/remix-scene-component.tsx index 1f6daf81dab0..374c0c4add69 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,12 +7,14 @@ 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) => { @@ -49,7 +51,7 @@ export const RemixSceneComponent = React.memo((props: React.PropsWithChildren - + ) }) 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)[] +} From 4eb2101d02d06fa281726da3f534145c00f48631 Mon Sep 17 00:00:00 2001 From: Balazs Bajorics <2226774+balazsbajorics@users.noreply.github.com> Date: Tue, 12 Dec 2023 14:26:36 +0100 Subject: [PATCH 6/7] fix react warning --- .../canvas/ui-jsx-canvas-renderer/remix-scene-component.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 374c0c4add69..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 @@ -21,7 +21,7 @@ export const RemixSceneComponent = React.memo((props: React.PropsWithChildren - + ) }) From aa2c66fe9d758a5f6a962f0177cca910dc3523e3 Mon Sep 17 00:00:00 2001 From: Balazs Bajorics <2226774+balazsbajorics@users.noreply.github.com> Date: Tue, 12 Dec 2023 14:27:38 +0100 Subject: [PATCH 7/7] adding test for getLoadContext --- .../remix/remix-rendering.spec.browser2.tsx | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) 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({