From 5eb72f00a43fff9eaca955b0a0f71faa06c59cb6 Mon Sep 17 00:00:00 2001
From: Balazs Bajorics <2226774+balazsbajorics@users.noreply.github.com>
Date: Fri, 11 Oct 2024 14:34:23 +0200
Subject: [PATCH] Revert "Use node module resolution (#6425)" (#6524)
This reverts commit da6bf80b0b51fa75d9c2e92a60144a37467685a6.
**Problem:**
When loading the Hydrogen demo store, an infinite recursion happens:
![image](https://github.com/user-attachments/assets/ee1a761d-c950-43c3-a633-f62ec580d722)
---
.../components/custom-code/code-file.spec.ts | 90 +--
.../src/components/custom-code/code-file.ts | 3 -
.../core/es-modules/evaluator/evaluator.ts | 1 -
.../package-manager/module-resolution-esm.ts | 228 ------
.../module-resolution-utils.ts | 216 ------
.../package-manager/module-resolution.spec.ts | 130 +---
.../package-manager/module-resolution.ts | 665 ++++++++++--------
.../package-manager/package-manager.ts | 14 +-
.../module-resolution-examples.json | 53 +-
.../core/frameworks/framework-hooks-vite.ts | 5 +-
editor/src/utils/path-utils.ts | 13 +-
11 files changed, 412 insertions(+), 1006 deletions(-)
delete mode 100644 editor/src/core/es-modules/package-manager/module-resolution-esm.ts
delete mode 100644 editor/src/core/es-modules/package-manager/module-resolution-utils.ts
diff --git a/editor/src/components/custom-code/code-file.spec.ts b/editor/src/components/custom-code/code-file.spec.ts
index 7cb6d28929d9..4562aa933e10 100644
--- a/editor/src/components/custom-code/code-file.spec.ts
+++ b/editor/src/components/custom-code/code-file.spec.ts
@@ -26,12 +26,7 @@ import {
textFileContents,
unparsed,
} from '../../core/shared/project-file-types'
-import {
- addFileToProjectContents,
- contentsToTree,
- getTextFileByPath,
- type ProjectContentTreeRoot,
-} from '../assets'
+import { addFileToProjectContents, getTextFileByPath } from '../assets'
import type { ExportsInfo, MultiFileBuildResult } from '../../core/workers/common/worker-types'
import { createBuiltInDependenciesList } from '../../core/es-modules/package-manager/built-in-dependencies-list'
@@ -58,15 +53,6 @@ function transpileCode(
const appJSCode =
"\nimport * as React from 'react'\nimport {\n Ellipse,\n HelperFunctions,\n Image,\n NodeImplementations,\n Rectangle,\n Text,\n View,\n} from 'utopia-api'\nimport {\n colorTheme,\n Button,\n Dialog,\n Icn,\n Icons,\n LargerIcons,\n FunctionIcons,\n MenuIcons,\n Isolator,\n TabComponent,\n Tooltip,\n ActionSheet,\n Avatar,\n ControlledTextArea,\n Title,\n H1,\n H2,\n H3,\n Subdued,\n InspectorSectionHeader,\n InspectorSubsectionHeader,\n FlexColumn,\n FlexRow,\n ResizableFlexColumn,\n PopupList,\n Section,\n SectionTitleRow,\n SectionBodyArea,\n UtopiaListSelect,\n UtopiaListItem,\n CheckboxInput,\n NumberInput,\n StringInput,\n OnClickOutsideHOC,\n} from 'uuiui'\n\nexport var canvasMetadata = {\n specialNodes: [],\n nodeMetadata: {},\n scenes: [\n {\n component: 'App',\n frame: { height: 812, left: 0, width: 375, top: 0 },\n props: { layout: { top: 0, left: 0, bottom: 0, right: 0 } },\n container: { layoutSystem: 'pinSystem' },\n },\n ],\n elementMetadata: {},\n}\n\nexport var App = (props) => {\n return (\n \n )\n}\n\n"
-const SampleSingleFileProjectContents: ProjectContentTreeRoot = contentsToTree({
- '/app.js': textFile(
- textFileContents(appJSCode, unparsed, RevisionsState.CodeAhead),
- null,
- null,
- 0,
- ),
-})
-
const SampleSingleFileBuildResult = transpileCode(['/app.js'], {
'/app.js': appJSCode,
})
@@ -98,40 +84,15 @@ const SampleSingleFileExportsInfo = [
},
]
-const multiFileAppJSCode =
- "\nimport * as React from 'react'\nimport {\n Ellipse,\n HelperFunctions,\n Image,\n NodeImplementations,\n Rectangle,\n Text,\n View,\n} from 'utopia-api'\nimport {\n colorTheme,\n Button,\n Dialog,\n Icn,\n Icons,\n LargerIcons,\n FunctionIcons,\n MenuIcons,\n Isolator,\n TabComponent,\n Tooltip,\n ActionSheet,\n Avatar,\n ControlledTextArea,\n Title,\n H1,\n H2,\n H3,\n Subdued,\n InspectorSectionHeader,\n InspectorSubsectionHeader,\n FlexColumn,\n FlexRow,\n ResizableFlexColumn,\n PopupList,\n Section,\n SectionTitleRow,\n SectionBodyArea,\n UtopiaListSelect,\n UtopiaListItem,\n CheckboxInput,\n NumberInput,\n StringInput,\n OnClickOutsideHOC,\n} from 'uuiui'\n\nexport var canvasMetadata = {\n specialNodes: [],\n nodeMetadata: {},\n scenes: [\n {\n component: 'App',\n frame: { height: 812, left: 0, width: 375, top: 0 },\n props: { layout: { top: 0, left: 0, bottom: 0, right: 0 } },\n container: { layoutSystem: 'pinSystem' },\n },\n ],\n elementMetadata: {},\n}\n\n\nexport var App = (props) => {\n return (\n \n )\n}\n\n"
-const multiFileComponentsJSCode =
- "// component library\nimport * as React from 'react'\nimport { Text, View } from 'utopia-api'\n\nexport default (props) => (\n \n \n \n)\n\nexport const LABEL = 'press me! 😉'\n\nexport const ComponentWithProps = (props) => {\n return (\n
\n {(props.text + ' ').repeat(props.num)}\n
\n )\n}\n\nComponentWithProps.propertyControls = {\n text: {\n type: 'string',\n title: 'Title',\n defaultValue: 'Change me',\n },\n num: {\n type: 'number',\n title: 'amount',\n defaultValue: 2,\n },\n pink: {\n type: 'boolean',\n title: 'Enabled',\n defaultValue: true,\n },\n}\n\n"
-const multiFilePreviewJSXCode =
- 'import * as React from "react";\nimport * as ReactDOM from "react-dom";\nimport { App } from "../app";\n\nconst root = document.getElementById("root");\nif (root != null) {\nReactDOM.render(, root);\n}'
-
-const SampleMultiFileProjectContents: ProjectContentTreeRoot = contentsToTree({
- '/app.js': textFile(
- textFileContents(multiFileAppJSCode, unparsed, RevisionsState.CodeAhead),
- null,
- null,
- 0,
- ),
- '/src/components.js': textFile(
- textFileContents(multiFileComponentsJSCode, unparsed, RevisionsState.CodeAhead),
- null,
- null,
- 0,
- ),
- '/public/preview.jsx': textFile(
- textFileContents(multiFilePreviewJSXCode, unparsed, RevisionsState.CodeAhead),
- null,
- null,
- 0,
- ),
-})
-
const SampleMultiFileBuildResult = transpileCode(
['/app.js', '/src/components.js', '/public/preview.jsx'],
{
- '/app.js': multiFileAppJSCode,
- '/src/components.js': multiFileComponentsJSCode,
- '/public/preview.jsx': multiFilePreviewJSXCode,
+ '/app.js':
+ "\nimport * as React from 'react'\nimport {\n Ellipse,\n HelperFunctions,\n Image,\n NodeImplementations,\n Rectangle,\n Text,\n View,\n} from 'utopia-api'\nimport {\n colorTheme,\n Button,\n Dialog,\n Icn,\n Icons,\n LargerIcons,\n FunctionIcons,\n MenuIcons,\n Isolator,\n TabComponent,\n Tooltip,\n ActionSheet,\n Avatar,\n ControlledTextArea,\n Title,\n H1,\n H2,\n H3,\n Subdued,\n InspectorSectionHeader,\n InspectorSubsectionHeader,\n FlexColumn,\n FlexRow,\n ResizableFlexColumn,\n PopupList,\n Section,\n SectionTitleRow,\n SectionBodyArea,\n UtopiaListSelect,\n UtopiaListItem,\n CheckboxInput,\n NumberInput,\n StringInput,\n OnClickOutsideHOC,\n} from 'uuiui'\n\nexport var canvasMetadata = {\n specialNodes: [],\n nodeMetadata: {},\n scenes: [\n {\n component: 'App',\n frame: { height: 812, left: 0, width: 375, top: 0 },\n props: { layout: { top: 0, left: 0, bottom: 0, right: 0 } },\n container: { layoutSystem: 'pinSystem' },\n },\n ],\n elementMetadata: {},\n}\n\n\nexport var App = (props) => {\n return (\n \n )\n}\n\n",
+ '/src/components.js':
+ "// component library\nimport * as React from 'react'\nimport { Text, View } from 'utopia-api'\n\nexport default (props) => (\n \n \n \n)\n\nexport const LABEL = 'press me! 😉'\n\nexport const ComponentWithProps = (props) => {\n return (\n \n {(props.text + ' ').repeat(props.num)}\n
\n )\n}\n\nComponentWithProps.propertyControls = {\n text: {\n type: 'string',\n title: 'Title',\n defaultValue: 'Change me',\n },\n num: {\n type: 'number',\n title: 'amount',\n defaultValue: 2,\n },\n pink: {\n type: 'boolean',\n title: 'Enabled',\n defaultValue: true,\n },\n}\n\n",
+ '/public/preview.jsx':
+ 'import * as React from "react";\nimport * as ReactDOM from "react-dom";\nimport { App } from "../app";\n\nconst root = document.getElementById("root");\nif (root != null) {\nReactDOM.render(, root);\n}',
},
)
@@ -255,25 +216,14 @@ const SampleExportsInfoWithError = [
},
]
-const exceptionThrowingCodeJSFile = "export const boom = (() => {\n throw Error('booom')\n})()\n"
-
-const SampleExceptionThrowingProjectContents: ProjectContentTreeRoot = contentsToTree({
- '/src/code.js': textFile(
- textFileContents(exceptionThrowingCodeJSFile, unparsed, RevisionsState.CodeAhead),
- null,
- null,
- 0,
- ),
-})
-
const SampleBuildResultWithException = transpileCode(['/src/code.js'], {
- '/src/code.js': exceptionThrowingCodeJSFile,
+ '/src/code.js': "export const boom = (() => {\n throw Error('booom')\n})()\n",
})
const SampleExportsInfoWithException = [
{
filename: '/src/code.js',
- code: exceptionThrowingCodeJSFile,
+ code: "export const boom = (() => {\n throw Error('booom')\n})()\n",
exportTypes: {
boom: {
type: 'never',
@@ -440,9 +390,7 @@ describe('Creating require function', () => {
createBuiltInDependenciesList(null),
)
- expect(
- codeResultCache.curriedRequireFn(SampleSingleFileProjectContents)('/', './app', false),
- ).toMatchSnapshot()
+ expect(codeResultCache.curriedRequireFn({})('/', './app', false)).toMatchSnapshot()
})
it('Creates require function for multi file build result', () => {
const codeResultCache = generateCodeResultCache(
@@ -455,16 +403,8 @@ describe('Creating require function', () => {
createBuiltInDependenciesList(null),
)
- expect(
- codeResultCache.curriedRequireFn(SampleMultiFileProjectContents)('/', './app', false),
- ).toMatchSnapshot()
- expect(
- codeResultCache.curriedRequireFn(SampleMultiFileProjectContents)(
- '/',
- './src/components',
- false,
- ),
- ).toMatchSnapshot()
+ expect(codeResultCache.curriedRequireFn({})('/', './app', false)).toMatchSnapshot()
+ expect(codeResultCache.curriedRequireFn({})('/', './src/components', false)).toMatchSnapshot()
})
it('Require throws exception for module code', () => {
const codeResultCache = generateCodeResultCache(
@@ -478,11 +418,7 @@ describe('Creating require function', () => {
)
expect(() =>
- codeResultCache.curriedRequireFn(SampleExceptionThrowingProjectContents)(
- '/',
- './src/code',
- false,
- ),
+ codeResultCache.curriedRequireFn({})('/', './src/code', false),
).toThrowErrorMatchingSnapshot()
})
it('Require throws exception for import from non-existing module', () => {
diff --git a/editor/src/components/custom-code/code-file.ts b/editor/src/components/custom-code/code-file.ts
index 373b778925d3..db71fa3fa70c 100644
--- a/editor/src/components/custom-code/code-file.ts
+++ b/editor/src/components/custom-code/code-file.ts
@@ -355,9 +355,6 @@ export function generateCodeResultCache(
delete evaluationCache[fileToDelete]
}
- // FIXME This actually isn't doing anything ever, because it appears that `updatedModules` above is always empty,
- // however the require and resolve functions are still used, so we need to figure out which part of this code is
- // still alive and which is dead
// MUTATION ALERT! This function is mutating editorState.nodeModules.files by inserting the project files into it.
incorporateBuildResult(nodeModules, projectContents, projectModules)
diff --git a/editor/src/core/es-modules/evaluator/evaluator.ts b/editor/src/core/es-modules/evaluator/evaluator.ts
index 2c1ff0134993..089651ed16a7 100644
--- a/editor/src/core/es-modules/evaluator/evaluator.ts
+++ b/editor/src/core/es-modules/evaluator/evaluator.ts
@@ -92,7 +92,6 @@ function evaluateJs(
function firstErrorHandler(error: Error): void {
if (isEsModuleError(error)) {
- // This is effectively implementing `MAYBE_DETECT_AND_LOAD` from https://nodejs.org/api/modules.html#all-together
const { transpiledCode, sourceMap } = transformToCommonJS(filePath, moduleCode)
evaluateWithHandler(transpiledCode, sourceMap, secondErrorHandler)
} else {
diff --git a/editor/src/core/es-modules/package-manager/module-resolution-esm.ts b/editor/src/core/es-modules/package-manager/module-resolution-esm.ts
deleted file mode 100644
index 4483cf4bedca..000000000000
--- a/editor/src/core/es-modules/package-manager/module-resolution-esm.ts
+++ /dev/null
@@ -1,228 +0,0 @@
-import { getPartsFromPath, makePathFromParts, normalizePath } from '../../../utils/path-utils'
-import type { ParseResult } from '../../../utils/value-parser-utils'
-import { foldEither } from '../../shared/either'
-import { isEsCodeFile } from '../../shared/project-file-types'
-import type {
- ExportsField,
- FileForPathParts,
- FileLookupFn,
- FileLookupResult,
- PartialPackageJsonDefinition,
-} from './module-resolution-utils'
-import {
- findClosestPackageScopeToPath,
- getPartialPackageJson,
- isResolveSuccess,
- resolveNotPresent,
-} from './module-resolution-utils'
-
-// Using the logic from https://nodejs.org/api/modules.html#all-together
-// This is separated to emphasize that these functions act an ESM compatibility
-// layer in CommonJS
-
-// LOAD_PACKAGE_IMPORTS(X, DIR)
-export function loadPackageImports(
- path: string,
- originPath: string,
- packageJsonLookupFn: FileForPathParts,
- lookupFn: FileLookupFn,
-): FileLookupResult {
- // Loading a module from the imports field mapping https://nodejs.org/api/packages.html#imports
- // Find the closest package scope SCOPE to DIR.
- const { packageJsonFileResult, packageJsonDir } = findClosestPackageScopeToPath(
- originPath,
- packageJsonLookupFn,
- )
-
- // If no scope was found, return
- if (
- !(isResolveSuccess(packageJsonFileResult) && isEsCodeFile(packageJsonFileResult.success.file))
- ) {
- return resolveNotPresent
- }
-
- let possiblePackageJson: ParseResult
- try {
- possiblePackageJson = getPartialPackageJson(packageJsonFileResult.success.file.fileContents)
- } catch {
- return resolveNotPresent
- }
-
- return foldEither(
- (_) => resolveNotPresent,
- (packageJson) => {
- // If the SCOPE/package.json "imports" is null or undefined, return
- const importsEntry = packageJson['imports']
- if (importsEntry == null) {
- return resolveNotPresent
- }
-
- return packageImportsExportsResolve(path, importsEntry, packageJsonDir, lookupFn)
- },
- possiblePackageJson,
- )
-}
-
-// LOAD_PACKAGE_EXPORTS(X, DIR)
-export function loadPackageExports(
- path: string,
- originPath: string,
- packageJsonLookupFn: FileForPathParts,
- lookupFn: FileLookupFn,
-): FileLookupResult {
- // Try to interpret X as a combination of NAME and SUBPATH where the name
- // may have a @scope/ prefix and the subpath begins with a slash (`/`)
- const regex = /((?:@[\w|\-|\.]+\/)*[\w|\-|\.]+)(\/[\w|\-|\.]*)*/
- const matches = path.match(regex)
- if (matches == null) {
- // If X does not match this pattern, return
- return resolveNotPresent
- }
-
- const [_combined, name, subpath] = matches
-
- // If DIR/NAME/package.json is not a file, return
- const pathToModule = normalizePath([...getPartsFromPath(originPath), name])
- const packageJsonPathParts = pathToModule.concat('package.json')
- const packageJsonFileResult = packageJsonLookupFn(packageJsonPathParts)
- if (
- !(isResolveSuccess(packageJsonFileResult) && isEsCodeFile(packageJsonFileResult.success.file))
- ) {
- return resolveNotPresent
- }
-
- // Parse DIR/NAME/package.json, and look for "exports" field
- let possiblePackageJson: ParseResult
- try {
- possiblePackageJson = getPartialPackageJson(packageJsonFileResult.success.file.fileContents)
- } catch {
- return resolveNotPresent
- }
-
- return foldEither(
- (_) => resolveNotPresent,
- (packageJson) => {
- // If "exports" is null or undefined, return.
- const exportsEntry = packageJson['exports']
- if (exportsEntry == null) {
- return resolveNotPresent
- }
-
- return packageImportsExportsResolve(
- `.${subpath}`,
- exportsEntry,
- makePathFromParts(pathToModule),
- lookupFn,
- )
- },
- possiblePackageJson,
- )
-}
-
-// LOAD_PACKAGE_SELF(X, DIR)
-export function loadPackageSelf(
- path: string,
- originPath: string,
- packageJsonLookupFn: FileForPathParts,
- lookupFn: FileLookupFn,
-): FileLookupResult {
- // Find the closest package scope SCOPE to DIR.
- const { packageJsonFileResult, packageJsonDir } = findClosestPackageScopeToPath(
- originPath,
- packageJsonLookupFn,
- )
-
- // If no scope was found, return.
- if (
- !(isResolveSuccess(packageJsonFileResult) && isEsCodeFile(packageJsonFileResult.success.file))
- ) {
- return resolveNotPresent
- }
-
- let possiblePackageJson: ParseResult
- try {
- possiblePackageJson = getPartialPackageJson(packageJsonFileResult.success.file.fileContents)
- } catch {
- return resolveNotPresent
- }
- return foldEither(
- (_) => resolveNotPresent,
- (packageJson) => {
- // If "exports" is null or undefined, return.
- const exportsEntry = packageJson['exports']
- if (exportsEntry == null) {
- return resolveNotPresent
- }
-
- // If "name" is not the first segment of X, return
- const nameEntry = packageJson.name
- if (nameEntry == null || !path.startsWith(nameEntry)) {
- return resolveNotPresent
- }
-
- return packageImportsExportsResolve(
- `.${path.slice(nameEntry.length)}`,
- exportsEntry,
- packageJsonDir,
- lookupFn,
- )
- },
- possiblePackageJson,
- )
-}
-
-// PACKAGE_IMPORTS_EXPORTS_RESOLVE
-export function packageImportsExportsResolve(
- path: string,
- importsOrExportsEntry: ExportsField,
- packageJsonDir: string,
- lookupFn: FileLookupFn,
-): FileLookupResult {
- // FIXME partial path lookup because the exports field could be along the lines of `{ '@thing': { stuff: { ... }} }`
- // in which case we would want to match that against the import `@thing/stuff`
- const importsOrExportsResult =
- typeof importsOrExportsEntry === 'string' ? importsOrExportsEntry : importsOrExportsEntry[path]
- if (importsOrExportsResult == null) {
- return resolveNotPresent
- }
-
- if (typeof importsOrExportsResult === 'string') {
- // Lookup the import relative to the package.json we have found
- return lookupFn(importsOrExportsResult, packageJsonDir)
- }
-
- // Otherwise importsOrExportsResult is an object, so now we need to check for the specific keys we're interested in
- const defaultImportsOrExportsEntry = importsOrExportsResult['default']
- const browserImportsOrExportsEntry = importsOrExportsResult['browser'] // The nodejs algorithm uses `node` here
- const requireImportsOrExportsEntry = importsOrExportsResult['require']
-
- if (defaultImportsOrExportsEntry != null) {
- const fileToLookup =
- typeof defaultImportsOrExportsEntry === 'string'
- ? defaultImportsOrExportsEntry
- : defaultImportsOrExportsEntry['default']
- if (fileToLookup != null) {
- return lookupFn(fileToLookup, packageJsonDir)
- }
- }
- if (browserImportsOrExportsEntry != null) {
- const fileToLookup =
- typeof browserImportsOrExportsEntry === 'string'
- ? browserImportsOrExportsEntry
- : browserImportsOrExportsEntry['default']
- if (fileToLookup != null) {
- return lookupFn(fileToLookup, packageJsonDir)
- }
- }
- if (requireImportsOrExportsEntry != null) {
- const fileToLookup =
- typeof requireImportsOrExportsEntry === 'string'
- ? requireImportsOrExportsEntry
- : requireImportsOrExportsEntry['default']
- if (fileToLookup != null) {
- return lookupFn(fileToLookup, packageJsonDir)
- }
- }
-
- return resolveNotPresent
-}
diff --git a/editor/src/core/es-modules/package-manager/module-resolution-utils.ts b/editor/src/core/es-modules/package-manager/module-resolution-utils.ts
deleted file mode 100644
index 0a2bc939216b..000000000000
--- a/editor/src/core/es-modules/package-manager/module-resolution-utils.ts
+++ /dev/null
@@ -1,216 +0,0 @@
-import type { MapLike } from 'typescript'
-import { getPartsFromPath, makePathFromParts, normalizePath } from '../../../utils/path-utils'
-import type { ParseResult } from '../../../utils/value-parser-utils'
-import {
- optionalObjectKeyParser,
- parseAlternative,
- parseFalse,
- parseNull,
- parseObject,
- parseString,
-} from '../../../utils/value-parser-utils'
-import { applicative6Either } from '../../shared/either'
-import { setOptionalProp } from '../../shared/object-utils'
-import type { ESCodeFile, ESRemoteDependencyPlaceholder } from '../../shared/project-file-types'
-
-import LRU from 'lru-cache'
-
-export type ImportsExportsObjectField = MapLike<
- string | MapLike>
->
-export type ExportsField = string | ImportsExportsObjectField
-
-export interface PartialPackageJsonDefinition {
- name?: string
- main?: string
- module?: string
- browser?: string | MapLike
- imports?: ImportsExportsObjectField
- exports?: ExportsField
-}
-
-const partialPackageJsonCache: LRU> = new LRU({
- max: 20,
-})
-export function getPartialPackageJson(contents: string): ParseResult {
- const fromCache = partialPackageJsonCache.get(contents)
- if (fromCache == null) {
- const jsonParsed = JSON.parse(contents)
- const result = parsePartialPackageJsonDefinition(jsonParsed)
- partialPackageJsonCache.set(contents, result)
- return result
- } else {
- return fromCache
- }
-}
-
-export interface ResolveSuccess {
- type: 'RESOLVE_SUCCESS'
- success: T
-}
-
-export function resolveSuccess(success: T): ResolveSuccess {
- return {
- type: 'RESOLVE_SUCCESS',
- success: success,
- }
-}
-
-export interface ResolveNotPresent {
- type: 'RESOLVE_NOT_PRESENT'
-}
-
-export const resolveNotPresent: ResolveNotPresent = {
- type: 'RESOLVE_NOT_PRESENT',
-}
-
-export interface ResolveSuccessIgnoreModule {
- type: 'RESOLVE_SUCCESS_IGNORE_MODULE'
-}
-
-export function isResolveSuccessIgnoreModule(
- resolveResult: ResolveResult,
-): resolveResult is ResolveSuccessIgnoreModule {
- return resolveResult.type === 'RESOLVE_SUCCESS_IGNORE_MODULE'
-}
-
-export const resolveSuccessIgnoreModule: ResolveSuccessIgnoreModule = {
- type: 'RESOLVE_SUCCESS_IGNORE_MODULE',
-}
-
-export type ResolveResult = ResolveNotPresent | ResolveSuccessIgnoreModule | ResolveSuccess
-
-export function isResolveSuccess(
- resolveResult: ResolveResult,
-): resolveResult is ResolveSuccess {
- return resolveResult.type === 'RESOLVE_SUCCESS'
-}
-
-export function isResolveNotPresent(
- resolveResult: ResolveResult,
-): resolveResult is ResolveNotPresent {
- return resolveResult.type === 'RESOLVE_NOT_PRESENT'
-}
-
-export interface FoundFile {
- path: string
- file: ESCodeFile | ESRemoteDependencyPlaceholder
-}
-
-export type FileLookupResult = ResolveResult
-
-export function fileLookupResult(
- path: string,
- file: ESCodeFile | ESRemoteDependencyPlaceholder | null,
-): FileLookupResult {
- if (file == null) {
- return resolveNotPresent
- } else {
- return resolveSuccess({
- path: path,
- file: file,
- })
- }
-}
-
-export type FileLookupFn = (path: string, importOrigin: string) => FileLookupResult
-export type FileForPathParts = (pathParts: string[]) => FileLookupResult
-
-const importsExportsObjectParser = parseObject(
- parseAlternative>>(
- [
- parseString,
- parseObject(
- parseAlternative>(
- [
- parseString,
- parseNull,
- parseObject(
- parseAlternative(
- [parseString, parseNull],
- `package.imports and package.exports replacement entries must be either string or null`,
- ),
- ),
- ],
- `package.imports and package.exports replacement entries must be either string or null`,
- ),
- ),
- ],
- 'package.imports and package.exports object must be an object with type {[key: string]: string | null}',
- ),
-)
-
-export const exportsParser = parseAlternative(
- [parseString, importsExportsObjectParser],
- 'package.imports and package.exports field must either be a string or an object with type {[key: string]: string | null}',
-)
-
-export function parsePartialPackageJsonDefinition(
- value: unknown,
-): ParseResult {
- return applicative6Either(
- (name, main, module, browser, imports, exports) => {
- let result: PartialPackageJsonDefinition = {}
- setOptionalProp(result, 'name', name)
- setOptionalProp(result, 'main', main)
- setOptionalProp(result, 'module', module)
- setOptionalProp(result, 'browser', browser)
- setOptionalProp(result, 'imports', imports)
- setOptionalProp(result, 'exports', exports)
- return result
- },
- optionalObjectKeyParser(parseString, 'name')(value),
- optionalObjectKeyParser(parseString, 'main')(value),
- optionalObjectKeyParser(parseString, 'module')(value),
- optionalObjectKeyParser(
- parseAlternative>(
- [
- parseString,
- parseObject(
- parseAlternative(
- [parseString, parseFalse],
- `package.browser replacement entries must be either string or 'false' for ignoring a package`,
- ),
- ),
- ],
- 'package.browser field must either be a string or an object with type {[key: string]: string | false}',
- ),
- 'browser',
- )(value),
- optionalObjectKeyParser(importsExportsObjectParser, 'imports')(value),
- optionalObjectKeyParser(exportsParser, 'exports')(value),
- )
-}
-
-export type PackageLookupResult = {
- packageJsonFileResult: FileLookupResult
- packageJsonDir: string
-}
-export function findClosestPackageScopeToPath(
- path: string,
- localFileLookup: FileForPathParts,
-): PackageLookupResult {
- const originPathPartsToTest = normalizePath(getPartsFromPath(path))
- const allPossiblePackageJsonPaths = [['package.json']]
- .concat(
- originPathPartsToTest.map((_part, index) =>
- originPathPartsToTest.slice(0, index + 1).concat('package.json'),
- ),
- )
- .reverse()
-
- for (const possiblePath of allPossiblePackageJsonPaths) {
- const packageJsonFileResult = localFileLookup(possiblePath)
- if (isResolveSuccess(packageJsonFileResult)) {
- return {
- packageJsonFileResult: packageJsonFileResult,
- packageJsonDir: makePathFromParts(possiblePath.slice(0, -1)),
- }
- }
- }
-
- return {
- packageJsonFileResult: resolveNotPresent,
- packageJsonDir: '',
- }
-}
diff --git a/editor/src/core/es-modules/package-manager/module-resolution.spec.ts b/editor/src/core/es-modules/package-manager/module-resolution.spec.ts
index eb394f94f2c2..aa22947b72e7 100644
--- a/editor/src/core/es-modules/package-manager/module-resolution.spec.ts
+++ b/editor/src/core/es-modules/package-manager/module-resolution.spec.ts
@@ -7,8 +7,7 @@ import {
unparsed,
} from '../../shared/project-file-types'
import * as moduleResolutionExamples from '../test-cases/module-resolution-examples.json'
-import { resolveModule } from './module-resolution'
-import { isResolveSuccess } from './module-resolution-utils'
+import { isResolveSuccess, resolveModule } from './module-resolution'
import { createNodeModules } from './test-utils'
const sampleProjectContents: ProjectContentTreeRoot = contentsToTree({
@@ -34,34 +33,6 @@ const sampleProjectContents: ProjectContentTreeRoot = contentsToTree({
null,
0,
),
- '/package.json': textFile(
- textFileContents(
- `
- {
- "name": "sample-project",
- "exports": {
- "./exported-module": "./exported-module-main.js"
- },
- "imports": {
- "#dep-object": {
- "default": "./src/dep-object.js"
- },
- "#dep-simple": "./src/dep-simple.js"
- }
- }`,
- unparsed,
- RevisionsState.CodeAhead,
- ),
- null,
- null,
- 0,
- ),
- '/exported-module-main.js': textFile(
- textFileContents('export const ExportedModuleMain = 1', unparsed, RevisionsState.CodeAhead),
- null,
- null,
- 0,
- ),
'/a/some/nested/file.js': textFile(
textFileContents('export const Cake = "tasty"', unparsed, RevisionsState.CodeAhead),
null,
@@ -80,18 +51,6 @@ const sampleProjectContents: ProjectContentTreeRoot = contentsToTree({
null,
0,
),
- '/src/dep-object.js': textFile(
- textFileContents('export const DepObject = 1', unparsed, RevisionsState.CodeAhead),
- null,
- null,
- 0,
- ),
- '/src/dep-simple.js': textFile(
- textFileContents('export const DepSimple = 1', unparsed, RevisionsState.CodeAhead),
- null,
- null,
- 0,
- ),
'/src/icon.png': {
type: 'ASSET_FILE',
},
@@ -101,12 +60,6 @@ const sampleProjectContents: ProjectContentTreeRoot = contentsToTree({
null,
0,
),
- '/utopia/layout.utopia.js': textFile(
- textFileContents('export const Layout = 1', unparsed, RevisionsState.CodeAhead),
- null,
- null,
- 0,
- ),
})
describe('ES Package Manager Module Resolution', () => {
@@ -291,69 +244,20 @@ describe('ES Package Manager Module Resolution', () => {
expect(resolveResult.type).toEqual('RESOLVE_SUCCESS_IGNORE_MODULE')
})
- it('resolves package exports for local package', () => {
- expect(resolve('/src/app.js', 'sample-project/exported-module')).toEqual(
- '/exported-module-main.js',
- )
- })
-
- it('resolves package imports simple for local package', () => {
- expect(resolve('/src/app.js', '#dep-simple')).toEqual('/src/dep-simple.js')
- })
- it('resolves package imports object for local package', () => {
- expect(resolve('/src/app.js', '#dep-object')).toEqual('/src/dep-object.js')
- })
-
- it('resolves package main entry for node module with exports with no subpath', () => {
- expect(resolve('/src/app.js', 'module-with-exports-and-imports')).toEqual(
- '/node_modules/module-with-exports-and-imports/index.main.js',
- )
- })
-
- it('resolves package exports object main entry for node module', () => {
- expect(resolve('/src/app.js', 'module-with-exports-and-imports-objects')).toEqual(
- '/node_modules/module-with-exports-and-imports-objects/index.main.js',
- )
- })
- it('resolves package exports object submodule export for node module', () => {
- expect(resolve('/src/app.js', 'module-with-exports-and-imports-objects/submodule')).toEqual(
- '/node_modules/module-with-exports-and-imports-objects/src/submodule.js',
- )
- })
-
- it('resolves package imports for node module', () => {
- expect(
- resolve('/node_modules/module-with-exports-and-imports/src/something.js', '#dep'),
- ).toEqual('/node_modules/module-with-exports-and-imports/dep.js')
- })
- it('resolves package imports object for node module', () => {
- expect(
- resolve('/node_modules/module-with-exports-and-imports-objects/src/something.js', '#dep'),
- ).toEqual('/node_modules/module-with-exports-and-imports-objects/dep.js')
- })
-
- it('loads self references', () => {
- expect(
- resolve(
- '/node_modules/my-package-with-package-root/src/deep-folder/moduleA.js',
- 'my-package-with-package-root',
- ),
- ).toEqual('/node_modules/my-package-with-package-root/index.js')
- expect(
- resolve(
- '/node_modules/my-package-with-package-root/src/deep-folder/moduleA.js',
- 'my-package-with-package-root/src/other-folder/moduleB',
- ),
- ).toEqual('/node_modules/my-package-with-package-root/src/other-folder/moduleB.js')
- })
-
- it('resolves clsx', () => {
- expect(resolve('/app/components/hydrogen/Button.jsx', 'clsx')).toEqual(
- '/node_modules/clsx/dist/clsx.js',
- )
- })
-
- it('resolves /utopia/layout.utopia', () => {
- expect(resolve('/utopia/text.utopia.js', './layout.utopia')).toEqual('/utopia/layout.utopia.js')
- })
+ // it('loads self references', () => {
+ // expect(
+ // resolveModule(
+ // createNodeModules(moduleResolutionExamples.contents),
+ // 'my-package-with-package-root/src/deep-folder/moduleA.js',
+ // 'my-package-with-package-root',
+ // ),
+ // ).toEqual('my-package-with-package-root/index.js')
+ // expect(
+ // resolveModule(
+ // createNodeModules(moduleResolutionExamples.contents),
+ // 'my-package-with-package-root/src/deep-folder/moduleA.js',
+ // 'my-package-with-package-root/src/other-folder/moduleB',
+ // ),
+ // ).toEqual('my-package-with-package-root/src/other-folder/moduleB.js')
+ // })
})
diff --git a/editor/src/core/es-modules/package-manager/module-resolution.ts b/editor/src/core/es-modules/package-manager/module-resolution.ts
index b570990de635..8904005017ff 100644
--- a/editor/src/core/es-modules/package-manager/module-resolution.ts
+++ b/editor/src/core/es-modules/package-manager/module-resolution.ts
@@ -1,261 +1,180 @@
-import type { MapLike } from 'typescript'
-import type { ProjectContentTreeRoot } from '../../../components/assets'
-import { getContentsTreeFileFromElements } from '../../../components/assets'
-import { applyFilePathMappingsToFilePath } from '../../../core/workers/common/project-file-utils'
-import {
- getParentDirectory,
- getPartsFromPath,
- makePathFromParts,
- normalizePath,
- stripTrailingSlash,
-} from '../../../utils/path-utils'
+import type {
+ ProjectFile,
+ NodeModules,
+ ESCodeFile,
+ ESRemoteDependencyPlaceholder,
+} from '../../shared/project-file-types'
+import { isEsCodeFile, esCodeFile } from '../../shared/project-file-types'
import type { ParseResult } from '../../../utils/value-parser-utils'
-import { getFilePathMappings } from '../../model/project-file-utils'
-import { dropLast, last } from '../../shared/array-utils'
+import {
+ optionalObjectKeyParser,
+ parseString,
+ parseAlternative,
+ parseObject,
+ parseFalse,
+} from '../../../utils/value-parser-utils'
import type { Either } from '../../shared/either'
-import { foldEither, isLeft, isRight, left, right } from '../../shared/either'
-import type { NodeModules, ProjectFile } from '../../shared/project-file-types'
-import { esCodeFile, isEsCodeFile } from '../../shared/project-file-types'
-import type { BuiltInDependencies } from './built-in-dependencies-list'
-import { loadPackageSelf, loadPackageImports, loadPackageExports } from './module-resolution-esm'
-import type {
- FileForPathParts,
- FileLookupFn,
- FileLookupResult,
- PartialPackageJsonDefinition,
-} from './module-resolution-utils'
import {
- fileLookupResult,
- getPartialPackageJson,
- isResolveSuccess,
- resolveNotPresent,
- resolveSuccessIgnoreModule,
-} from './module-resolution-utils'
-
-// Using the logic from https://nodejs.org/api/modules.html#all-together
-
-function loadAsFile(
- projectContents: ProjectContentTreeRoot,
- pathParts: string[],
-): FileLookupResult {
- return abstractLoadAsFile(projectContentsFileForPathParts(projectContents), pathParts)
-}
-
-// LOAD_AS_FILE(X)
-function abstractLoadAsFile(
- fileForPathParts: FileForPathParts,
- pathParts: string[],
-): FileLookupResult {
- const normalisedPathParts = normalizePath(pathParts)
- const pathToFile = dropLast(normalisedPathParts)
- const lastPart = last(normalisedPathParts)
- if (lastPart == null) {
- return resolveNotPresent
- }
-
- // If X is a file, load X as its file extension format
- const asFileResult = fileForPathParts(normalisedPathParts)
- if (isResolveSuccess(asFileResult)) {
- return asFileResult
- }
-
- // If X.js(x) is a file
- const withJs = lastPart + '.js'
- const withJsResult = fileForPathParts(pathToFile.concat(withJs))
- if (isResolveSuccess(withJsResult)) {
- return withJsResult
- }
+ applicative3Either,
+ applicative4Either,
+ foldEither,
+ isLeft,
+ isRight,
+ left,
+ right,
+} from '../../shared/either'
+import { setOptionalProp } from '../../shared/object-utils'
+import type { ProjectContentTreeRoot } from '../../../components/assets'
+import { getContentsTreeFileFromElements } from '../../../components/assets'
+import { dropLast, last } from '../../shared/array-utils'
+import { getPartsFromPath, makePathFromParts, normalizePath } from '../../../utils/path-utils'
+import type { MapLike } from 'typescript'
- const withJsx = lastPart + '.jsx'
- const withJsxResult = fileForPathParts(pathToFile.concat(withJsx))
- if (isResolveSuccess(withJsxResult)) {
- return withJsxResult
- }
+import LRU from 'lru-cache'
+import type { BuiltInDependencies } from './built-in-dependencies-list'
+import { getFilePathMappings } from '../../model/project-file-utils'
+import { applyFilePathMappingsToFilePath } from '../../../core/workers/common/project-file-utils'
- // If X.json is a file, load X.json to a JavaScript Object
- const withJson = lastPart + '.json'
- const withJsonResult = fileForPathParts(pathToFile.concat(withJson))
- if (isResolveSuccess(withJsonResult)) {
- return withJsonResult
+const partialPackageJsonCache: LRU> = new LRU({
+ max: 20,
+})
+function getPartialPackageJson(contents: string): ParseResult {
+ const fromCache = partialPackageJsonCache.get(contents)
+ if (fromCache == null) {
+ const jsonParsed = JSON.parse(contents)
+ const result = parsePartialPackageJsonDefinition(jsonParsed)
+ partialPackageJsonCache.set(contents, result)
+ return result
+ } else {
+ return fromCache
}
+}
- return resolveNotPresent
+interface ResolveSuccess {
+ type: 'RESOLVE_SUCCESS'
+ success: T
}
-function processPackageJson(potentiallyJsonCode: string): string | null {
- let possiblePackageJson: ParseResult
- try {
- possiblePackageJson = getPartialPackageJson(potentiallyJsonCode)
- } catch {
- return null
+function resolveSuccess(success: T): ResolveSuccess {
+ return {
+ type: 'RESOLVE_SUCCESS',
+ success: success,
}
- return foldEither(
- (_) => null,
- (packageJson) => {
- // const moduleName: string | null = packageJson.name ?? null
- const browserEntry: string | MapLike | null = packageJson.browser ?? null
- const mainEntry: string | null = packageJson.main ?? null
- // const moduleEntry: string | null = packageJson.module ?? null
-
- if (browserEntry != null && typeof browserEntry === 'string') {
- return browserEntry
- } else if (mainEntry != null) {
- return mainEntry
- // } else if (moduleEntry != null) {
- // return moduleEntry
- // } else if (moduleName != null) {
- // return moduleName
- }
-
- return null
- },
- possiblePackageJson,
- )
}
-function loadAsDirectory(
- projectContents: ProjectContentTreeRoot,
- pathParts: string[],
-): FileLookupResult {
- return abstractLoadAsDirectory(projectContentsFileForPathParts(projectContents), pathParts)
+interface ResolveNotPresent {
+ type: 'RESOLVE_NOT_PRESENT'
}
-// LOAD_AS_DIRECTORY(X)
-function abstractLoadAsDirectory(
- fileForPathParts: FileForPathParts,
- pathParts: string[],
-): FileLookupResult {
- const normalisedPathParts = normalizePath(pathParts) // X
-
- // If X/package.json is a file
- const jsonFileResult = fileForPathParts(normalisedPathParts.concat('package.json'))
- if (isResolveSuccess(jsonFileResult) && isEsCodeFile(jsonFileResult.success.file)) {
- // Parse X/package.json, and look for "browser" or "main" field
- const mainEntryPath = processPackageJson(jsonFileResult.success.file.fileContents)
-
- if (mainEntryPath != null) {
- // let M = X + (json main field)
- const mainEntryPathParts = [...normalisedPathParts, ...getPartsFromPath(mainEntryPath)]
-
- // LOAD_AS_FILE(M)
- const mainEntryResult = abstractLoadAsFile(fileForPathParts, mainEntryPathParts)
- if (isResolveSuccess(mainEntryResult)) {
- return mainEntryResult
- }
-
- // LOAD_INDEX(M)
- const loadIndexMResult = loadIndex(fileForPathParts, mainEntryPathParts)
- if (isResolveSuccess(loadIndexMResult)) {
- return loadIndexMResult
- }
-
- // LOAD_INDEX(X) DEPRECATED - note that this is only deprecated in the case where "main" is truthy
- return loadIndex(fileForPathParts, normalisedPathParts)
- }
- }
-
- // If "main" is a falsy value
- // LOAD_INDEX(X)
- return loadIndex(fileForPathParts, normalisedPathParts)
+const resolveNotPresent: ResolveNotPresent = {
+ type: 'RESOLVE_NOT_PRESENT',
}
-// LOAD_INDEX(X)
-function loadIndex(fileForPathParts: FileForPathParts, pathParts: string[]): FileLookupResult {
- // 1. If X/index.js is a file
- // a. Find the closest package scope SCOPE to X.
- // b. If no scope was found, load X/index.js as a CommonJS module. STOP.
- // c. If the SCOPE/package.json contains "type" field,
- // 1. If the "type" field is "module", load X/index.js as an ECMAScript module. STOP.
- // 2. Else, load X/index.js as an CommonJS module. STOP.
- // Note: We don't attempt to load as an ECMAScript, as we can't - instead we'll try to load whatever
- // it is as CommonJS, and if it turns out to be an esm we'll attempt to transpile it to CommonJS
-
- const indexJSResult = abstractLoadAsFile(fileForPathParts, pathParts.concat('index.js'))
- if (isResolveSuccess(indexJSResult)) {
- return indexJSResult
- }
-
- // If X/index.json is a file, parse X/index.json to a JavaScript object
- return abstractLoadAsFile(fileForPathParts, pathParts.concat('index.json'))
-
- // If X/index.node is a file, load X/index.node as binary addon - not applicable
+interface ResolveSuccessIgnoreModule {
+ type: 'RESOLVE_SUCCESS_IGNORE_MODULE'
}
-// NODE_MODULES_PATHS
-function nodeModulesPaths(path: string): Array {
- // let PARTS = path split(START)
- const pathParts = getPartsFromPath(path)
+export function isResolveSuccessIgnoreModule(
+ resolveResult: ResolveResult,
+): resolveResult is ResolveSuccessIgnoreModule {
+ return resolveResult.type === 'RESOLVE_SUCCESS_IGNORE_MODULE'
+}
- // let DIRS = []
- let dirs: Array = []
+const resolveSuccessIgnoreModule: ResolveSuccessIgnoreModule = {
+ type: 'RESOLVE_SUCCESS_IGNORE_MODULE',
+}
- // The algorithm builds dirs in descending order, but we'll do it in ascending and reverse
- pathParts.forEach((part, index) => {
- // if PARTS[I] = "node_modules" CONTINUE
- if (part === 'node_modules') {
- return
- }
+type ResolveResult = ResolveNotPresent | ResolveSuccessIgnoreModule | ResolveSuccess
- // DIR = path join(PARTS[0 .. I] + "node_modules")
- const dir = makePathFromParts(pathParts.slice(0, index + 1).concat('node_modules'))
+export function isResolveSuccess(
+ resolveResult: ResolveResult,
+): resolveResult is ResolveSuccess {
+ return resolveResult.type === 'RESOLVE_SUCCESS'
+}
- // DIRS = DIR + DIRS
- dirs.push(dir)
- })
- dirs.reverse()
+export function isResolveNotPresent(
+ resolveResult: ResolveResult,
+): resolveResult is ResolveNotPresent {
+ return resolveResult.type === 'RESOLVE_NOT_PRESENT'
+}
- // return DIRS + GLOBAL_FOLDERS
- return dirs.concat('/node_modules/')
+interface FoundFile {
+ path: string
+ file: ESCodeFile | ESRemoteDependencyPlaceholder
}
-// LOAD_NODE_MODULES
-function loadNodeModules(
- nodeModules: NodeModules,
+export type FileLookupResult = ResolveResult
+
+function fileLookupResult(
path: string,
- originPath: string,
- lookupFn: FileLookupFn,
+ file: ESCodeFile | ESRemoteDependencyPlaceholder | null,
): FileLookupResult {
- const packageJsonLookupFn = nodeModulesFileForPathParts(nodeModules)
-
- // let DIRS = NODE_MODULES_PATHS(START)
- const dirs = nodeModulesPaths(originPath).reverse()
- const targetOrigins = [originPath, ...dirs]
+ if (file == null) {
+ return resolveNotPresent
+ } else {
+ return resolveSuccess({
+ path: path,
+ file: file,
+ })
+ }
+}
- for (const targetOrigin of targetOrigins) {
- // for each DIR in DIRS:
+type FileLookupFn = (path: string[], direct: boolean) => FileLookupResult
- // LOAD_PACKAGE_EXPORTS(X, DIR)
- const packageExportsResult = loadPackageExports(
- path,
- targetOrigin,
- packageJsonLookupFn,
- lookupFn,
- )
- if (isResolveSuccess(packageExportsResult)) {
- return packageExportsResult
+function fallbackLookup(
+ lookupFn: FileLookupFn,
+ path: Array,
+ direct: boolean,
+): FileLookupResult {
+ const asIsPath = normalizePath(path)
+ const pathToFile = dropLast(asIsPath)
+ const lastPart = last(asIsPath)
+ if (lastPart == null) {
+ return resolveNotPresent
+ } else {
+ const asIsResult = lookupFn(asIsPath, direct)
+ if (isResolveSuccess(asIsResult)) {
+ return asIsResult
}
- const pathForLookup = path.startsWith('/')
- ? path
- : `${stripTrailingSlash(targetOrigin)}/${path}`
+ if (!direct) {
+ const withJs = lastPart + '.js'
+ const withJsResult = lookupFn(pathToFile.concat(withJs), direct)
+ if (isResolveSuccess(withJsResult)) {
+ return withJsResult
+ }
- const pathParts = getPartsFromPath(pathForLookup)
- const normalisedPathParts = normalizePath(pathParts)
+ const withJsx = lastPart + '.jsx'
+ const withJsxResult = lookupFn(pathToFile.concat(withJsx), direct)
+ if (isResolveSuccess(withJsxResult)) {
+ return withJsxResult
+ }
- // LOAD_AS_FILE(DIR/X)
- const asFileResult = abstractLoadAsFile(packageJsonLookupFn, normalisedPathParts)
- if (isResolveSuccess(asFileResult)) {
- return asFileResult
+ // TODO this also needs JSON parsing
+ const withJson = lastPart + '.json'
+ const withJsonResult = lookupFn(pathToFile.concat(withJson), direct)
+ if (isResolveSuccess(withJsonResult)) {
+ return withJsonResult
+ }
}
- // LOAD_AS_DIRECTORY(DIR/X)
- const asDirectoryResult = abstractLoadAsDirectory(packageJsonLookupFn, normalisedPathParts)
- if (isResolveSuccess(asDirectoryResult)) {
- return asDirectoryResult
- }
+ return resolveNotPresent
}
+}
- return resolveNotPresent
+function nodeModulesFileLookup(
+ nodeModules: NodeModules,
+ path: Array,
+ direct: boolean,
+): FileLookupResult {
+ return fallbackLookup(
+ (innerPath: string[], innerDirect: boolean) => {
+ const filename = makePathFromParts(innerPath)
+ return fileLookupResult(filename, nodeModules[filename])
+ },
+ path,
+ direct,
+ )
}
function getProjectFileContentsAsString(file: ProjectFile): string | null {
@@ -274,52 +193,240 @@ function getProjectFileContentsAsString(file: ProjectFile): string | null {
}
}
-function projectContentsFileForPathParts(
+function projectContentsFileLookup(
projectContents: ProjectContentTreeRoot,
-): FileForPathParts {
- return (pathParts: string[]) => {
- const projectFile = getContentsTreeFileFromElements(projectContents, pathParts)
- if (projectFile == null) {
- return resolveNotPresent
- } else {
- const fileContents = getProjectFileContentsAsString(projectFile)
- if (fileContents == null) {
+ path: Array,
+ direct: boolean,
+): FileLookupResult {
+ return fallbackLookup(
+ (innerPath: string[]) => {
+ const projectFile = getContentsTreeFileFromElements(projectContents, innerPath)
+ if (projectFile == null) {
return resolveNotPresent
} else {
- const filename = makePathFromParts(pathParts)
- return fileLookupResult(filename, esCodeFile(fileContents, 'PROJECT_CONTENTS', filename))
+ const fileContents = getProjectFileContentsAsString(projectFile)
+ if (fileContents == null) {
+ return resolveNotPresent
+ } else {
+ const filename = makePathFromParts(innerPath)
+ return fileLookupResult(filename, esCodeFile(fileContents, 'PROJECT_CONTENTS', filename))
+ }
}
- }
- }
+ },
+ path,
+ direct,
+ )
}
-function nodeModulesFileForPathParts(nodeModules: NodeModules): FileForPathParts {
- return (pathParts: string[]) => {
- const filename = makePathFromParts(pathParts)
- const nodeModulesFile = nodeModules[filename]
- if (nodeModulesFile == null) {
+interface PartialPackageJsonDefinition {
+ name?: string
+ main?: string
+ module?: string
+ browser?: string | MapLike
+}
+
+export function parsePartialPackageJsonDefinition(
+ value: unknown,
+): ParseResult {
+ return applicative4Either(
+ (name, main, module, browser) => {
+ let result: PartialPackageJsonDefinition = {}
+ setOptionalProp(result, 'name', name)
+ setOptionalProp(result, 'main', main)
+ setOptionalProp(result, 'module', module)
+ setOptionalProp(result, 'browser', browser)
+ return result
+ },
+ optionalObjectKeyParser(parseString, 'name')(value),
+ optionalObjectKeyParser(parseString, 'main')(value),
+ optionalObjectKeyParser(parseString, 'module')(value),
+ optionalObjectKeyParser(
+ parseAlternative>(
+ [
+ parseString,
+ parseObject(
+ parseAlternative(
+ [parseString, parseFalse],
+ `package.browser replacement entries must be either string or 'false' for ignoring a package`,
+ ),
+ ),
+ ],
+ 'package.browser field must either be a string or an object with type {[key: string]: string}',
+ ),
+ 'browser',
+ )(value),
+ )
+}
+
+function processPackageJson(
+ potentiallyJsonCode: string,
+ containerFolder: string[],
+): ResolveResult> {
+ let possiblePackageJson: ParseResult
+ try {
+ possiblePackageJson = getPartialPackageJson(potentiallyJsonCode)
+ } catch {
+ return resolveNotPresent
+ }
+ return foldEither(
+ (_) => resolveNotPresent,
+ (packageJson) => {
+ const moduleName: string | null = packageJson.name ?? null
+ const browserEntry: string | MapLike | null = packageJson.browser ?? null
+ const mainEntry: string | null = packageJson.main ?? null
+ const moduleEntry: string | null = packageJson.module ?? null
+
+ if (browserEntry != null && typeof browserEntry === 'string') {
+ return resolveSuccess(
+ normalizePath([...containerFolder, ...getPartsFromPath(browserEntry)]),
+ )
+ } else if (mainEntry != null) {
+ return resolveSuccess(normalizePath([...containerFolder, ...getPartsFromPath(mainEntry)]))
+ } else if (moduleEntry != null) {
+ return resolveSuccess(normalizePath([...containerFolder, ...getPartsFromPath(moduleEntry)]))
+ } else if (moduleName != null) {
+ return resolveSuccess(normalizePath([...containerFolder, ...getPartsFromPath(moduleName)]))
+ } else if (containerFolder.length > 0) {
+ return resolveSuccess(normalizePath(containerFolder))
+ }
+
return resolveNotPresent
+ },
+ possiblePackageJson,
+ )
+}
+
+function resolvePackageJson(
+ fileLookupFn: FileLookupFn,
+ packageJsonFolder: string[],
+): FileLookupResult {
+ const normalizedFolderPath = normalizePath(packageJsonFolder)
+ const folderPackageJson = fileLookupFn([...normalizedFolderPath, 'package.json'], true)
+ if (isResolveSuccess(folderPackageJson) && isEsCodeFile(folderPackageJson.success.file)) {
+ const mainEntryPath = processPackageJson(
+ folderPackageJson.success.file.fileContents,
+ normalizedFolderPath,
+ )
+ if (isResolveSuccess(mainEntryPath)) {
+ // try loading the entry path as a file
+ const mainEntryResult = fileLookupFn(mainEntryPath.success, false)
+ if (isResolveSuccess(mainEntryResult)) {
+ return mainEntryResult
+ } else {
+ // fallback to loading it as a folder with an index.js
+ const indexJsPath = [...mainEntryPath.success, 'index']
+ return fileLookupFn(indexJsPath, false)
+ }
} else {
- return fileLookupResult(filename, nodeModulesFile)
+ return resolveNotPresent
}
}
+ return resolveNotPresent
}
-function findPackageJsonForPath(
- fileLookupFn: FileLookupFn,
- originPathParts: string[],
-): FileLookupResult {
+function findPackageJsonForPath(fileLookupFn: FileLookupFn, origin: string[]): FileLookupResult {
// 1. look for /package.json
- const packageJsonResult = fileLookupFn(`./package.json`, makePathFromParts(originPathParts))
+ const packageJsonResult = fileLookupFn([...origin, 'package.json'], true)
if (isResolveSuccess(packageJsonResult)) {
return packageJsonResult
} else {
// 2. repeat in the parent folder
- if (originPathParts.length === 0) {
+ if (origin.length === 0) {
// we exhausted all folders without success
return resolveNotPresent
} else {
- return findPackageJsonForPath(fileLookupFn, originPathParts.slice(0, -1))
+ return findPackageJsonForPath(fileLookupFn, origin.slice(0, -1))
+ }
+ }
+}
+
+function resolveNonRelativeModule(
+ fileLookupFn: FileLookupFn,
+ importOrigin: string[],
+ toImport: string[],
+): FileLookupResult {
+ const pathElements = [...importOrigin, 'node_modules', ...toImport]
+ // 1. look for ./node_modules/.js
+ const packageNameResult = fileLookupFn(pathElements, false)
+ if (isResolveSuccess(packageNameResult)) {
+ return packageNameResult
+ } else {
+ // 2. look for ./node_modules//package.json
+ const packageJsonResult = resolvePackageJson(fileLookupFn, pathElements)
+ if (isResolveSuccess(packageJsonResult)) {
+ return packageJsonResult
+ } else {
+ // 3. look for ./node_modules//index.js
+ const indexJsPath = [...pathElements, 'index']
+ const indexJSResult = fileLookupFn(indexJsPath, false)
+ if (isResolveSuccess(indexJSResult)) {
+ return indexJSResult
+ } else {
+ // 4. repeat in the parent folder
+ if (importOrigin.length === 0) {
+ // we exhausted all folders without success
+ return resolveNotPresent
+ } else {
+ return resolveNonRelativeModule(fileLookupFn, importOrigin.slice(0, -1), toImport)
+ }
+ }
+ }
+ }
+}
+
+function resolveRelativeModule(
+ fileLookupFn: FileLookupFn,
+ importOrigin: string[],
+ toImport: string[],
+): FileLookupResult {
+ const pathElements = [...importOrigin, ...toImport]
+ // 1. look for a file named
+ const importNameResult = fileLookupFn(pathElements, false)
+ if (isResolveSuccess(importNameResult)) {
+ return importNameResult
+ } else {
+ // 2. look for /package.json
+ const packageJsonResult = resolvePackageJson(fileLookupFn, pathElements)
+ if (isResolveSuccess(packageJsonResult)) {
+ return packageJsonResult
+ } else {
+ // 3. look for /index.js
+ return fileLookupFn([...pathElements, 'index'], false)
+ }
+ }
+}
+
+// Module resolution logic based on what Node / Typescript does https://www.typescriptlang.org/docs/handbook/module-resolution.html#node
+
+// TODO there's way more module resolution rules here https://parceljs.org/module_resolution.html#module-resolution
+
+// TODO an even more comprehensive writeup https://nodejs.org/api/modules.html#modules_all_together
+
+function resolveModuleInternal(
+ fileLookupFn: FileLookupFn,
+ importOrigin: string,
+ toImport: string,
+): FileLookupResult {
+ if (toImport.startsWith('/')) {
+ // absolute import
+ return resolveRelativeModule(
+ fileLookupFn,
+ [], // this import is relative to the root
+ getPartsFromPath(toImport),
+ )
+ } else {
+ if (toImport.startsWith('.')) {
+ return resolveRelativeModule(
+ fileLookupFn,
+ getPartsFromPath(importOrigin).slice(0, -1),
+ getPartsFromPath(toImport),
+ )
+ } else {
+ return resolveNonRelativeModule(
+ fileLookupFn,
+ getPartsFromPath(importOrigin).slice(0, -1),
+ getPartsFromPath(toImport),
+ )
}
}
}
@@ -375,64 +482,15 @@ function resolveModuleAndApplySubstitutions(
importOrigin: string,
toImport: string,
): FileLookupResult {
- // require(X) from module at path Y
- function lookupFn(path: string, innerImportOrigin: string): FileLookupResult {
- // 1 Check if it's a core module - this first step is irrelevant to us, since the browser has no core modules
- if (path.startsWith('/') || path.startsWith('./') || path.startsWith('../')) {
- // pathForLookup here is Y + X
- const pathForLookup = path.startsWith('/') // If X begins with '/' set Y to be the file system root
- ? path
- : `${stripTrailingSlash(innerImportOrigin)}/${path}`
- const pathParts = getPartsFromPath(pathForLookup)
-
- // LOAD_AS_FILE(Y + X)
- const projectContentsFileLookupResult = loadAsFile(projectContents, pathParts)
-
- if (isResolveSuccess(projectContentsFileLookupResult)) {
- return projectContentsFileLookupResult
- }
-
- // LOAD_AS_DIRECTORY(Y + X)
- const projectContentsDirectoryLookupResult = loadAsDirectory(projectContents, pathParts)
- if (isResolveSuccess(projectContentsDirectoryLookupResult)) {
- return projectContentsDirectoryLookupResult
- }
- } else if (path.startsWith('#')) {
- // If X begins with '#'
- // LOAD_PACKAGE_IMPORTS(X, dirname(Y))
- return loadPackageImports(
- path,
- innerImportOrigin,
- innerImportOrigin.startsWith('/node_modules')
- ? nodeModulesFileForPathParts(nodeModules)
- : projectContentsFileForPathParts(projectContents),
- lookupFn,
- )
- }
-
- // LOAD_PACKAGE_SELF(X, dirname(Y))
- const packageExportsResult = loadPackageSelf(
- path,
- innerImportOrigin,
- projectContentsFileForPathParts(projectContents),
- lookupFn,
- )
-
- if (isResolveSuccess(packageExportsResult)) {
- return packageExportsResult
+ function lookupFn(path: string[], direct: boolean): FileLookupResult {
+ const nodeModulesResult = nodeModulesFileLookup(nodeModules, path, direct)
+ if (isResolveSuccess(nodeModulesResult)) {
+ return nodeModulesResult
+ } else {
+ return projectContentsFileLookup(projectContents, path, direct)
}
-
- // LOAD_NODE_MODULES(X, dirname(Y)), returning the result here either way in place of `throw 'not fount'`
- return loadNodeModules(
- nodeModules,
- path,
- innerImportOrigin.startsWith('/node_modules') ? innerImportOrigin : '/node_modules',
- lookupFn,
- )
}
- // We first need to check for the presence of a `browser` field in the current module's package.json, as that
- // can suggest an alternative mapping to use for modules it is importing https://github.com/defunctzombie/package-browser-field-spec
const substitutedImport: Either = findSubstitutionsForImport(
lookupFn,
importOrigin,
@@ -447,8 +505,7 @@ function resolveModuleAndApplySubstitutions(
substitutedImport.value,
filePathMappings,
)
-
- return lookupFn(unAliasedImport, getParentDirectory(importOrigin))
+ return resolveModuleInternal(lookupFn, importOrigin, unAliasedImport)
}
}
diff --git a/editor/src/core/es-modules/package-manager/package-manager.ts b/editor/src/core/es-modules/package-manager/package-manager.ts
index e7971183db65..0e90fe32c94a 100644
--- a/editor/src/core/es-modules/package-manager/package-manager.ts
+++ b/editor/src/core/es-modules/package-manager/package-manager.ts
@@ -1,7 +1,12 @@
import type { NodeModules, ESCodeFile } from '../../shared/project-file-types'
import { isEsCodeFile, isEsRemoteDependencyPlaceholder } from '../../shared/project-file-types'
import type { RequireFn, TypeDefinitions } from '../../shared/npm-dependency-types'
-import { resolveModule } from './module-resolution'
+import {
+ isResolveNotPresent,
+ isResolveSuccess,
+ isResolveSuccessIgnoreModule,
+ resolveModule,
+} from './module-resolution'
import { evaluator } from '../evaluator/evaluator'
import { fetchMissingFileDependency } from './fetch-packages'
import type { EditorDispatch } from '../../../components/editor/action-types'
@@ -12,13 +17,10 @@ import { utopiaApiTypings } from './utopia-api-typings'
import { resolveBuiltInDependency } from './built-in-dependencies'
import type { ProjectContentTreeRoot } from '../../../components/assets'
import { applyLoaders } from '../../webpack-loaders/loaders'
+import { string } from 'prop-types'
+import { Either } from '../../shared/either'
import type { CurriedUtopiaRequireFn } from '../../../components/custom-code/code-file'
import type { BuiltInDependencies } from './built-in-dependencies-list'
-import {
- isResolveNotPresent,
- isResolveSuccess,
- isResolveSuccessIgnoreModule,
-} from './module-resolution-utils'
import type { FrameworkHooks } from '../../frameworks/framework-hooks'
import { getFrameworkHooks } from '../../frameworks/framework-hooks'
diff --git a/editor/src/core/es-modules/test-cases/module-resolution-examples.json b/editor/src/core/es-modules/test-cases/module-resolution-examples.json
index f094135cf88f..a0bf7334fa06 100644
--- a/editor/src/core/es-modules/test-cases/module-resolution-examples.json
+++ b/editor/src/core/es-modules/test-cases/module-resolution-examples.json
@@ -230,70 +230,25 @@
"isModule": false,
"requires": []
},
- "/node_modules/module-with-exports-and-imports/package.json": {
- "content": "{\"name\": \"module-with-exports-and-imports\", \"main\": \"index.main.js\", \"exports\": \"index.exports.js\", \"imports\": {\"#dep\": \"./dep.js\"}}",
- "isModule": false,
- "requires": []
- },
- "/node_modules/module-with-exports-and-imports/index.main.js": {
+ "my-package-with-package-root/src/deep-folder/moduleA.js": {
"content": "exports.cica = 'kitten';",
"isModule": false,
"requires": []
},
- "/node_modules/module-with-exports-and-imports/dep.js": {
+ "my-package-with-package-root/src/other-folder/moduleB.js": {
"content": "exports.cica = 'kitten';",
"isModule": false,
"requires": []
},
- "/node_modules/module-with-exports-and-imports-objects/package.json": {
- "content": "{\"name\": \"module-with-exports-and-imports-objects\", \"main\": \"index.main.js\", \"exports\": {\".\": \"index.exports.js\", \"./submodule\": \"./src/submodule.js\"}, \"imports\": {\"#dep\": {\"default\": \"./dep.js\"}}}",
- "isModule": false,
- "requires": []
- },
- "/node_modules/module-with-exports-and-imports-objects/index.main.js": {
+ "my-package-with-package-root/index.js": {
"content": "exports.cica = 'kitten';",
"isModule": false,
"requires": []
},
- "/node_modules/module-with-exports-and-imports-objects/dep.js": {
- "content": "exports.cica = 'kitten';",
- "isModule": false,
- "requires": []
- },
- "/node_modules/module-with-exports-and-imports-objects/src/submodule.js": {
- "content": "exports.cica = 'kitten';",
- "isModule": false,
- "requires": []
- },
- "/node_modules/my-package-with-package-root/src/deep-folder/moduleA.js": {
- "content": "exports.cica = 'kitten';",
- "isModule": false,
- "requires": []
- },
- "/node_modules/my-package-with-package-root/src/other-folder/moduleB.js": {
- "content": "exports.cica = 'kitten';",
- "isModule": false,
- "requires": []
- },
- "/node_modules/my-package-with-package-root/index.js": {
- "content": "exports.cica = 'kitten';",
- "isModule": false,
- "requires": []
- },
- "/node_modules/my-package-with-package-root/package.json": {
+ "my-package-with-package-root/package.json": {
"content": "{\"name\": \"my-package-with-package-root\", \"main\": \".index.js\"}",
"isModule": false,
"requires": []
- },
- "/node_modules/clsx/package.json": {
- "content": "{\"name\": \"clsx\", \"main\": \"dist/clsx.js\", \"exports\": {\".\": {\"default\": { \"default\": \"./dist/clsx.js\"}}}}",
- "isModule": false,
- "requires": []
- },
- "/node_modules/clsx/dist/clsx.js": {
- "content": "exports.cica = 'kitten';",
- "isModule": false,
- "requires": []
}
},
"dependency": {
diff --git a/editor/src/core/frameworks/framework-hooks-vite.ts b/editor/src/core/frameworks/framework-hooks-vite.ts
index fd6fd2992af5..c5b67b7cfe2f 100644
--- a/editor/src/core/frameworks/framework-hooks-vite.ts
+++ b/editor/src/core/frameworks/framework-hooks-vite.ts
@@ -9,9 +9,8 @@ import type { ComponentToImport, CreationDataFromProject } from '../model/storyb
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-utils'
-import { isResolveSuccess } from '../es-modules/package-manager/module-resolution-utils'
-import { resolveModule } from '../es-modules/package-manager/module-resolution'
+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 {
diff --git a/editor/src/utils/path-utils.ts b/editor/src/utils/path-utils.ts
index 33fbc1fd77e9..13dad8741a71 100644
--- a/editor/src/utils/path-utils.ts
+++ b/editor/src/utils/path-utils.ts
@@ -33,7 +33,7 @@ export function getParentDirectory(filepath: string): string {
return makePathFromParts(getPartsFromPath(filepath).slice(0, -1))
}
-type SubPathCache = Map
+type SubPathCache = { [key: string]: PathCache }
interface PathCache {
cachedString: string
@@ -47,7 +47,7 @@ function pathCache(cachedString: string, subPathCache: SubPathCache): PathCache
}
}
-let rootPathCache: SubPathCache = new Map()
+let rootPathCache: SubPathCache = {}
function getPathFromCache(parts: Array): string {
let workingSubCache: SubPathCache = rootPathCache
@@ -55,14 +55,15 @@ function getPathFromCache(parts: Array): string {
let partsSoFar: Array = []
for (const part of parts) {
partsSoFar.push(part)
- if (workingSubCache.has(part)) {
- workingPathCache = workingSubCache.get(part)!
+ if (part in workingSubCache) {
+ // The `in` check above proves this does not return `undefined`.
+ workingPathCache = workingSubCache[part]!
workingSubCache = workingPathCache.subPathCache
} else {
const cachedString = `/${partsSoFar.join('/')}`
- const newPathCache = pathCache(cachedString, new Map())
+ const newPathCache = pathCache(cachedString, {})
workingPathCache = newPathCache
- workingSubCache.set(part, newPathCache)
+ workingSubCache[part] = newPathCache
workingSubCache = newPathCache.subPathCache
}
}