-
Notifications
You must be signed in to change notification settings - Fork 172
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(vite): fix vite project import (#6420)
**Problem:** Trying to import a [vanilla Vite project](https://github.com/liady/utopia-vite-project) fails today, with multiple issues, and results in a broken editor. **Fix:** Create a `FrameworkHooks` interface that the Vite specific code can implement, to hook into storyboard creation as well as module resolution ([`frameworks-hooks-vite.ts`](https://github.com/concrete-utopia/utopia/pull/6420/files#diff-b640da014227f136acea49663e81e9106c84da8f6e2c305ed21b51615c8048fe)). <img width="1083" alt="image" src="https://github.com/user-attachments/assets/dee615a5-b218-4c94-a349-386c7c9ccc24"> ### Creation of `storyboard.js` ([`storyboard-utils.ts`](https://github.com/concrete-utopia/utopia/pull/6420/files#diff-a071a27ce64f0de07b4bcf1da78fb0ba2113311d20653a726272561f0314902b)): * Detect main html file (`index.html`) if present, extract from it root element class name, root element id (`root`) and main script module (`main.js`) * From main script module, if present, extract main app candidate (`<App />`) and relevant css imports (`./index.css`) * Have the `storyboard.js` file import correctly the main app component, as well as relevant css imports. Assign id and class name to the `Scene`, if found. ### Module resolution fallbacks ([`package-manager.ts`](https://github.com/concrete-utopia/utopia/pull/6420/files#diff-a48ec59247957af2197227e3d2c2c2e5156fd31589c4981df5828ca15ac9db5b)) * Have a framework-specific fallback for cases like `/vite.svg`, that can lookup root files in the public folder as well. ### Fixes in other PRs: * #6445 - Prevent css root dimensions (`*-width`, `*-height`) from affecting the canvas itself. **Manual Tests:** I hereby swear that: - [x] I opened a hydrogen project and it loaded - [x] I could navigate to various routes in Preview mode
- Loading branch information
Showing
10 changed files
with
380 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import type { CreationDataFromProject } from '../model/storyboard-utils' | ||
import type { FrameworkHooks } from './framework-hooks' | ||
|
||
export class DefaultFrameworkHooks implements FrameworkHooks { | ||
detect(): boolean { | ||
return true | ||
} | ||
|
||
onProjectImport(): CreationDataFromProject | null { | ||
return null | ||
} | ||
|
||
onResolveModuleNotPresent(): string | null { | ||
return null | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
import type { NodeModules } from '../shared/project-file-types' | ||
import type { FrameworkHooks } from './framework-hooks' | ||
import { getProjectFileByFilePath, isProjectContentFile } from '../../components/assets' | ||
import type { Imports } from '../shared/project-file-types' | ||
import { forEachParseSuccess, isEmptyImportDetails, isTextFile } from '../shared/project-file-types' | ||
import type { ProjectContentTreeRoot } from 'utopia-shared/src/types' | ||
import { getFilePathMappings } from '../model/project-file-utils' | ||
import type { ComponentToImport, CreationDataFromProject } from '../model/storyboard-utils' | ||
import { namedComponentToImport, PossiblyMainComponentNames } from '../model/storyboard-utils' | ||
import { mergeImports } from '../workers/common/project-file-utils' | ||
import { absolutePathFromRelativePath } from '../../utils/path-utils' | ||
import type { FileLookupResult } from '../es-modules/package-manager/module-resolution' | ||
import { isResolveSuccess, resolveModule } from '../es-modules/package-manager/module-resolution' | ||
import { getMainScriptElement, getRootElement, parseHtml } from '../shared/dom-utils' | ||
|
||
export class ViteFrameworkHooks implements FrameworkHooks { | ||
detect(projectContents: ProjectContentTreeRoot): boolean { | ||
return hasViteConfig(projectContents) | ||
} | ||
|
||
onProjectImport(projectContents: ProjectContentTreeRoot): CreationDataFromProject | null { | ||
return getCreationDataFromProject(projectContents) | ||
} | ||
|
||
onResolveModuleNotPresent( | ||
projectContents: ProjectContentTreeRoot, | ||
nodeModules: NodeModules, | ||
importOrigin: string, | ||
toImport: string, | ||
): string | null { | ||
if (isInRootPath(toImport)) { | ||
const publicResolveResult = resolveModuleFromPublicDir( | ||
projectContents, | ||
nodeModules, | ||
importOrigin, | ||
toImport, | ||
) | ||
if (isResolveSuccess(publicResolveResult)) { | ||
return publicResolveResult.success.path | ||
} | ||
} | ||
return null | ||
} | ||
} | ||
|
||
const configFileNames = ['vite.config.ts', 'vite.config.js', 'vite.config.mjs'] | ||
|
||
function hasViteConfig(projectContents: ProjectContentTreeRoot): boolean { | ||
return configFileNames.some((fileName) => projectContents[fileName] != null) | ||
} | ||
|
||
export function getCreationDataFromProject( | ||
projectContents: ProjectContentTreeRoot, | ||
): CreationDataFromProject { | ||
const componentsToImport: ComponentToImport[] = [] | ||
let extraImports: Imports = {} | ||
const creationDataFromHtml = extractCreationDataFromMainHtmlFile(projectContents) | ||
if (creationDataFromHtml.maybeMainScriptFilePath != null) { | ||
const mainScriptFile = getProjectFileByFilePath( | ||
projectContents, | ||
creationDataFromHtml.maybeMainScriptFilePath, | ||
) | ||
|
||
if (mainScriptFile != null && isTextFile(mainScriptFile)) { | ||
forEachParseSuccess((success) => { | ||
// check for import candidates | ||
function toAbsolutePath(importSource: string): string { | ||
if (creationDataFromHtml.maybeMainScriptFilePath != null) { | ||
return absolutePathFromRelativePath( | ||
creationDataFromHtml.maybeMainScriptFilePath, | ||
false, | ||
importSource, | ||
) | ||
} | ||
return importSource | ||
} | ||
Object.entries(success.imports).forEach(([importSource, importDetails]) => { | ||
// "import App from '...'" | ||
if ( | ||
importDetails.importedWithName != null && | ||
PossiblyMainComponentNames.includes(importDetails.importedWithName) | ||
) { | ||
componentsToImport.push( | ||
namedComponentToImport( | ||
toAbsolutePath(importSource), | ||
true, | ||
'default', | ||
importDetails.importedWithName, | ||
), | ||
) | ||
} | ||
// "import { App } from '...'" | ||
importDetails.importedFromWithin.forEach((importAlias) => { | ||
if (PossiblyMainComponentNames.includes(importAlias.alias)) { | ||
componentsToImport.push( | ||
namedComponentToImport( | ||
toAbsolutePath(importSource), | ||
true, | ||
importAlias.name, | ||
importAlias.alias, | ||
), | ||
) | ||
} | ||
}) | ||
// "import '...'" | ||
if (isEmptyImportDetails(importDetails)) { | ||
extraImports = mergeImports( | ||
creationDataFromHtml.maybeMainScriptFilePath ?? '', | ||
getFilePathMappings(projectContents), | ||
extraImports, | ||
{ | ||
[toAbsolutePath(importSource)]: importDetails, | ||
}, | ||
).imports | ||
} | ||
}) | ||
}, mainScriptFile.fileContents.parsed) | ||
} | ||
} | ||
return { | ||
maybeRootElementId: creationDataFromHtml.maybeRootElementId, | ||
maybeRootElementClass: creationDataFromHtml.maybeRootElementClass, | ||
componentsToImport: componentsToImport, | ||
extraImports: extraImports, | ||
} | ||
} | ||
|
||
// TODO: read from vite config | ||
const MAIN_HTML_FILE_NAME = 'index.html' | ||
|
||
function extractCreationDataFromMainHtmlFile(projectContents: ProjectContentTreeRoot): { | ||
maybeRootElementId?: string | ||
maybeRootElementClass?: string | ||
maybeMainScriptFilePath?: string | ||
} { | ||
const indexHtmlFile = projectContents[MAIN_HTML_FILE_NAME] | ||
if ( | ||
indexHtmlFile != null && | ||
isProjectContentFile(indexHtmlFile) && | ||
isTextFile(indexHtmlFile.content) | ||
) { | ||
// try to parse the html file | ||
const doc = parseHtml(indexHtmlFile.content.fileContents.code) | ||
if (doc != null) { | ||
const rootElement = getRootElement(doc) | ||
const mainScriptElement = getMainScriptElement(doc) | ||
return { | ||
maybeRootElementId: rootElement?.id, | ||
maybeRootElementClass: rootElement?.className, | ||
maybeMainScriptFilePath: mainScriptElement?.getAttribute('src') ?? undefined, | ||
} | ||
} | ||
} | ||
return {} | ||
} | ||
|
||
// TODO: read from vite config | ||
const PUBLIC_FOLDER_PATH = 'public' | ||
|
||
function isInRootPath(toImport: string, publicDir: string = PUBLIC_FOLDER_PATH): boolean { | ||
return toImport.startsWith('/') && !toImport.startsWith(`/${publicDir}/`) | ||
} | ||
|
||
function resolveModuleFromPublicDir( | ||
projectContents: ProjectContentTreeRoot, | ||
nodeModules: NodeModules, | ||
importOrigin: string, | ||
toImport: string, | ||
publicDir: string = PUBLIC_FOLDER_PATH, | ||
): FileLookupResult { | ||
const publicPath = `/${publicDir}/${toImport}`.replace('//', '/').replace('/./', '/') | ||
return resolveModule(projectContents, nodeModules, importOrigin, publicPath) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import type { ProjectContentTreeRoot } from 'utopia-shared/src/types' | ||
import type { CreationDataFromProject } from '../model/storyboard-utils' | ||
import type { NodeModules } from '../shared/project-file-types' | ||
import { ViteFrameworkHooks } from './framework-hooks-vite' | ||
import { DefaultFrameworkHooks } from './framework-hooks-default' | ||
|
||
export interface FrameworkHooks { | ||
detect: (projectContents: ProjectContentTreeRoot) => boolean | ||
onProjectImport: (projectContents: ProjectContentTreeRoot) => CreationDataFromProject | null | ||
onResolveModuleNotPresent: ( | ||
projectContents: ProjectContentTreeRoot, | ||
nodeModules: NodeModules, | ||
importOrigin: string, | ||
toImport: string, | ||
) => string | null | ||
} | ||
|
||
const defaultFrameworkHooks = new DefaultFrameworkHooks() | ||
const frameworkHooks: Array<FrameworkHooks> = [new ViteFrameworkHooks(), defaultFrameworkHooks] | ||
|
||
export function getFrameworkHooks(projectContents: ProjectContentTreeRoot): FrameworkHooks { | ||
return ( | ||
frameworkHooks.find((framework) => framework.detect(projectContents)) ?? defaultFrameworkHooks | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.