diff --git a/editor/src/components/canvas/canvas-utils.ts b/editor/src/components/canvas/canvas-utils.ts index cafcc228ee0c..3350e9d918b1 100644 --- a/editor/src/components/canvas/canvas-utils.ts +++ b/editor/src/components/canvas/canvas-utils.ts @@ -69,6 +69,7 @@ import { isParseSuccess, isTextFile } from '../../core/shared/project-file-types import { applyUtopiaJSXComponentsChanges, getDefaultExportedTopLevelElement, + getFilePathMappings, getUtopiaJSXComponentsFromSuccess, isRemixSceneElement, } from '../../core/model/project-file-utils' @@ -1700,8 +1701,15 @@ export function getValidElementPaths( resolve: (importOrigin: string, toImport: string) => Either, getRemixValidPathsGenerationContext: (path: ElementPath) => RemixValidPathsGenerationContext, ): Array { + const filePathMappings = getFilePathMappings(projectContents) const { topLevelElements, imports } = getParseSuccessForFilePath(filePath, projectContents) - const importSource = importedFromWhere(filePath, topLevelElementName, topLevelElements, imports) + const importSource = importedFromWhere( + filePathMappings, + filePath, + topLevelElementName, + topLevelElements, + imports, + ) if (importSource != null) { let originTopLevelName = getTopLevelName(importSource, topLevelElementName) const resolvedImportSource = resolve(filePath, importSource.filePath) diff --git a/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-component-renderer.tsx b/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-component-renderer.tsx index 12d42dfc24db..945100fe72b2 100644 --- a/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-component-renderer.tsx +++ b/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-component-renderer.tsx @@ -48,6 +48,7 @@ import type { ComponentRendererComponent } from './component-renderer-component' import { mapArrayToDictionary } from '../../../core/shared/array-utils' import { assertNever } from '../../../core/shared/utils' import { addFakeSpyEntry } from './ui-jsx-canvas-spy-wrapper' +import type { FilePathMappings } from '../../../core/model/project-file-utils' function tryToGetInstancePath( maybePath: ElementPath | null, @@ -158,6 +159,7 @@ export function createComponentRendererComponent(params: { highlightBounds: highlightBounds, editedText: rerenderUtopiaContext.editedText, variablesInScope: {}, + filePathMappings: rerenderUtopiaContext.filePathMappings, }, undefined, codeError, @@ -228,6 +230,7 @@ export function createComponentRendererComponent(params: { code: code, highlightBounds: highlightBounds, editedText: rerenderUtopiaContext.editedText, + filePathMappings: rerenderUtopiaContext.filePathMappings, } const buildResult = React.useRef(null) diff --git a/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-contexts.tsx b/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-contexts.tsx index 27972ae9454a..6e33e1425683 100644 --- a/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-contexts.tsx +++ b/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-contexts.tsx @@ -7,6 +7,7 @@ import { left } from '../../../core/shared/either' import type { ElementPath } from '../../../core/shared/project-file-types' import type { ProjectContentTreeRoot } from '../../assets' import type { TransientFilesState, UIFileBase64Blobs } from '../../editor/store/editor-state' +import type { FilePathMappings } from '../../../core/model/project-file-utils' export interface MutableUtopiaCtxRefData { [filePath: string]: { @@ -32,6 +33,7 @@ interface RerenderUtopiaContextProps { canvasIsLive: boolean shouldIncludeCanvasRootInTheSpy: boolean editedText: ElementPath | null + filePathMappings: FilePathMappings } export const RerenderUtopiaCtxAtom = atomWithPubSub({ @@ -42,6 +44,7 @@ export const RerenderUtopiaCtxAtom = atomWithPubSub( canvasIsLive: false, shouldIncludeCanvasRootInTheSpy: false, editedText: null, + filePathMappings: [], }, }) diff --git a/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-element-renderer-utils.tsx b/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-element-renderer-utils.tsx index 2b8842a3fae5..376ac12923a6 100644 --- a/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-element-renderer-utils.tsx +++ b/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-element-renderer-utils.tsx @@ -78,6 +78,7 @@ import { import { RemixSceneComponent } from './remix-scene-component' import { STEGANOGRAPHY_ENABLED, isFeatureEnabled } from '../../../utils/feature-switches' import { jsxElementChildToText } from './jsx-element-child-to-text' +import type { FilePathMappings } from '../../../core/model/project-file-utils' export interface RenderContext { rootScope: MapLike @@ -98,6 +99,7 @@ export interface RenderContext { highlightBounds: HighlightBoundsForUids | null editedText: ElementPath | null variablesInScope: VariableData + filePathMappings: FilePathMappings } export function createLookupRender( @@ -704,6 +706,7 @@ function renderJSXElement( highlightBounds, editedText, variablesInScope, + filePathMappings, } = renderContext const createChildrenElement = (child: JSXElementChild): React.ReactChild => { const childPath = optionalMap((path) => EP.appendToPath(path, getUtopiaID(child)), elementPath) @@ -729,7 +732,7 @@ function renderJSXElement( // elements from scope and import to confirm it's not a top level element. const importedFrom = elementIsFragment ? null - : importedFromWhere(filePath, jsx.name.baseVariable, [], imports) + : importedFromWhere(filePathMappings, filePath, jsx.name.baseVariable, [], imports) const isElementImportedFromModule = (moduleName: string, name: string) => !elementIsIntrinsic && 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 18cf24ad1b9e..2c2ab7b637a7 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 @@ -35,6 +35,7 @@ import { applyBlockReturnFunctions, } from '../../../core/shared/dom-utils' import { emptySet } from '../../../core/shared/set-utils' +import { getFilePathMappings } from '../../../core/model/project-file-utils' const emptyFileBlobs: UIFileBase64Blobs = {} @@ -65,6 +66,7 @@ export function createExecutionScope( topLevelJsxComponents: Map requireResult: MapLike } { + const filePathMappings = getFilePathMappings(projectContents) if (!(filePath in topLevelComponentRendererComponents.current)) { // we make sure that the ref has an entry for this filepath topLevelComponentRendererComponents.current[filePath] = {} @@ -148,6 +150,7 @@ export function createExecutionScope( highlightBounds: highlightBounds, editedText: editedText, variablesInScope: {}, + filePathMappings: filePathMappings, }, null, propertiesFromParams, diff --git a/editor/src/components/canvas/ui-jsx-canvas.tsx b/editor/src/components/canvas/ui-jsx-canvas.tsx index d9f72b000d8f..e016cfa9105e 100644 --- a/editor/src/components/canvas/ui-jsx-canvas.tsx +++ b/editor/src/components/canvas/ui-jsx-canvas.tsx @@ -91,6 +91,7 @@ import { useAtom } from 'jotai' import { RemixNavigationAtom } from './remix/utopia-remix-root-component' import { IS_TEST_ENVIRONMENT } from '../../common/env-vars' import { listenForReactRouterErrors } from '../../core/shared/runtime-report-logs' +import { getFilePathMappings } from '../../core/model/project-file-utils' applyUIDMonkeyPatch() @@ -495,12 +496,15 @@ export const UiJsxCanvas = React.memo((props) validPaths: rootValidPathsSet, }) + const filePathMappings = getFilePathMappings(projectContents) + const rerenderUtopiaContextValue = useKeepShallowReferenceEquality({ hiddenInstances: hiddenInstances, displayNoneInstances: displayNoneInstances, canvasIsLive: canvasIsLive, shouldIncludeCanvasRootInTheSpy: props.shouldIncludeCanvasRootInTheSpy, editedText: props.editedText, + filePathMappings: filePathMappings, }) const utopiaProjectContextValue = useKeepShallowReferenceEquality({ diff --git a/editor/src/components/editor/import-utils.ts b/editor/src/components/editor/import-utils.ts index 6b0c184bb68a..2cd54405bd87 100644 --- a/editor/src/components/editor/import-utils.ts +++ b/editor/src/components/editor/import-utils.ts @@ -36,6 +36,11 @@ import { withUnderlyingTarget } from './store/editor-state' import * as EP from '../../core/shared/element-path' import { renameDuplicateImportsInMergeResolution } from '../../core/shared/import-shared-utils' import { getParseSuccessForFilePath } from '../canvas/canvas-utils' +import type { FilePathMappings } from '../../core/model/project-file-utils' +import { + applyFilePathMappingsToFilePath, + getFilePathMappings, +} from '../../core/model/project-file-utils' export function getRequiredImportsForElement( target: ElementPath, @@ -44,6 +49,7 @@ export function getRequiredImportsForElement( targetFilePath: string, builtInDependencies: BuiltInDependencies, ): ImportsMergeResolution { + const filePathMappings = getFilePathMappings(projectContents) return withUnderlyingTarget( target, projectContents, @@ -62,6 +68,7 @@ export function getRequiredImportsForElement( // Straight up ignore intrinsic elements as they wont be imported. if (!isIntrinsicElement(elem.name)) { const importedFromResult = importedFromWhere( + filePathMappings, underlyingFilePath, elem.name.baseVariable, topLevelElementsInOriginFile, @@ -144,6 +151,7 @@ export function getRequiredImportsForElement( type ImportedFromWhereResult = ImportInfo export function importedFromWhere( + filePathMappings: FilePathMappings, originFilePath: string, variableName: string, topLevelElements: Array, @@ -172,15 +180,16 @@ export function importedFromWhere( } for (const importSource of Object.keys(importsToSearch)) { const specificImport = importsToSearch[importSource] + const importSourceMapped = applyFilePathMappingsToFilePath(importSource, filePathMappings) if (specificImport.importedAs === variableName) { - return importedOrigin(importSource, variableName, null) + return importedOrigin(importSourceMapped, variableName, null) } if (specificImport.importedWithName === variableName) { - return importedOrigin(importSource, variableName, null) + return importedOrigin(importSourceMapped, variableName, null) } for (const fromWithin of specificImport.importedFromWithin) { if (fromWithin.alias === variableName) { - return importedOrigin(importSource, variableName, fromWithin.name) + return importedOrigin(importSourceMapped, variableName, fromWithin.name) } } } diff --git a/editor/src/core/property-controls/property-controls-utils.spec.ts b/editor/src/core/property-controls/property-controls-utils.spec.ts new file mode 100644 index 000000000000..8c92402e72e8 --- /dev/null +++ b/editor/src/core/property-controls/property-controls-utils.spec.ts @@ -0,0 +1,139 @@ +import type { ProjectContentTreeRoot } from '../../components/assets' +import { addFileToProjectContents } from '../../components/assets' +import type { + ComponentDescriptor, + PropertyControlsInfo, +} from '../../components/custom-code/code-file' +import { componentDescriptorFromDescriptorFile } from '../../components/custom-code/code-file' +import { simpleDefaultProject } from '../../sample-projects/sample-project-utils' +import { parseProjectContents } from '../../sample-projects/sample-project-utils.test-utils' +import { codeFile } from '../shared/project-file-types' +import { assertNever } from '../shared/utils' +import { getComponentDescriptorForTarget } from './property-controls-utils' +import * as EP from '../shared/element-path' + +describe('getComponentDescriptorForTarget', () => { + function getProjectContents(mappedPath: 'mapped-path' | 'regular-path'): ProjectContentTreeRoot { + let baseProjectContents: ProjectContentTreeRoot = simpleDefaultProject().projectContents + baseProjectContents = addFileToProjectContents( + baseProjectContents, + '/jsconfig.json', + codeFile( + `{ + "compilerOptions": { + "checkJs": true, + "jsx": "react-jsx", + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "baseUrl": ".", + "paths": { + "~/*": ["app/*"] + } + }, + "include": ["./**/*.d.ts", "./**/*.js", "./**/*.jsx"] +} +`, + null, + ), + ) + baseProjectContents = addFileToProjectContents( + baseProjectContents, + '/app/components.js', + codeFile( + ` +import * as React from 'react' +export const Component = () => { + return
+} +`, + null, + ), + ) + switch (mappedPath) { + case 'mapped-path': + baseProjectContents = addFileToProjectContents( + baseProjectContents, + '/src/app.js', + codeFile( + ` +import * as React from 'react' +import { Component } from '~/components' +export const App = (props) => { + return ( +
+ +
+ ) +}`, + null, + ), + ) + break + case 'regular-path': + baseProjectContents = addFileToProjectContents( + baseProjectContents, + '/src/app.js', + codeFile( + ` +import * as react from 'react' +import { Component } from '/app/components' +export const App = (props) => { + return ( +
+ +
+ ) +}`, + null, + ), + ) + break + default: + assertNever(mappedPath) + } + + return parseProjectContents(baseProjectContents) + } + const componentDescriptor: ComponentDescriptor = { + properties: {}, + supportsChildren: true, + preferredChildComponents: [], + variants: [], + source: componentDescriptorFromDescriptorFile('/components.utopia.js'), + focus: 'default', + inspector: { type: 'hidden' }, + emphasis: 'regular', + icon: 'component', + label: 'Component', + } + const propertyControlsInfo: PropertyControlsInfo = { + ['/app/components']: { + ['Component']: componentDescriptor, + }, + } + it('works with the regular path', () => { + const projectContents = getProjectContents('regular-path') + const actualResult = getComponentDescriptorForTarget( + EP.fromString(`sample-storyboard/sample-scene/sample-app:div/component`), + propertyControlsInfo, + projectContents, + ) + expect(actualResult).toEqual(componentDescriptor) + }) + it('works with a mapped path', () => { + const projectContents = getProjectContents('mapped-path') + const actualResult = getComponentDescriptorForTarget( + EP.fromString(`sample-storyboard/sample-scene/sample-app:div/component`), + propertyControlsInfo, + projectContents, + ) + expect(actualResult).toEqual(componentDescriptor) + }) +}) diff --git a/editor/src/core/property-controls/property-controls-utils.ts b/editor/src/core/property-controls/property-controls-utils.ts index c4877e31271f..836f1ca834e0 100644 --- a/editor/src/core/property-controls/property-controls-utils.ts +++ b/editor/src/core/property-controls/property-controls-utils.ts @@ -23,6 +23,7 @@ import { dropFileExtension } from '../shared/file-utils' import type { Styling } from 'utopia-api' import { StylingOptions } from 'utopia-api' import { intersection } from '../shared/set-utils' +import { getFilePathMappings } from '../model/project-file-utils' export function propertyControlsForComponentInFile( componentName: string, @@ -49,6 +50,7 @@ export function getPropertyControlsForTarget( propertyControlsInfo: PropertyControlsInfo, projectContents: ProjectContentTreeRoot, ): PropertyControls | null { + const filePathMappings = getFilePathMappings(projectContents) return withUnderlyingTarget( target, projectContents, @@ -61,6 +63,7 @@ export function getPropertyControlsForTarget( ) => { if (isJSXElement(element)) { const importedFrom = importedFromWhere( + filePathMappings, underlyingFilePath, element.name.baseVariable, success.topLevelElements, @@ -137,6 +140,7 @@ export function getComponentDescriptorForTarget( propertyControlsInfo: PropertyControlsInfo, projectContents: ProjectContentTreeRoot, ): ComponentDescriptor | null { + const filePathMappings = getFilePathMappings(projectContents) return withUnderlyingTarget( target, projectContents, @@ -149,6 +153,7 @@ export function getComponentDescriptorForTarget( ) => { if (isJSXElement(element)) { const importedFrom = importedFromWhere( + filePathMappings, underlyingFilePath, element.name.baseVariable, success.topLevelElements, diff --git a/editor/src/utils/utils.test-utils.tsx b/editor/src/utils/utils.test-utils.tsx index 2ffa82aed7fa..52cee2c61eb0 100644 --- a/editor/src/utils/utils.test-utils.tsx +++ b/editor/src/utils/utils.test-utils.tsx @@ -107,6 +107,7 @@ export const testRenderContext: RenderContext = { highlightBounds: null, editedText: null, variablesInScope: {}, + filePathMappings: [], } export function delay(time: number): Promise {