Skip to content

Commit 8a5b3ee

Browse files
authored
feat(import): check for server dependencies (#6603)
**Note for reviewers:** Other than refactors, tests and name changes, the **actual logic** is only in [requirement-server-packages.ts](https://github.com/concrete-utopia/utopia/compare/feat/check-server-dependencies?expand=1#diff-a2a88b7fd33ba102c98948e8efcb2c1771061df92f39e2b99af55560a1aceffc). (I had to change the resolution name from `'Found'` to `'Passed'` since here a passing check means that server packages weren't actually found) **PR details:** This PR adds a check for server dependencies to the import process - currently a very closed list, to be added more incrementally. For example when importing a Next project: <video src="https://github.com/user-attachments/assets/4db242d8-bb51-46e0-892c-d0a84d4c5231"></video> **Manual Tests:** I hereby swear that: - [X] I opened a hydrogen project and it loaded - [X] I could navigate to various routes in Play mode
1 parent 36213b9 commit 8a5b3ee

13 files changed

+177
-70
lines changed

editor/src/components/assets.spec.ts

+44-1
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
import * as FastCheck from 'fast-check'
22
import fastDeepEquals from 'fast-deep-equal'
33
import type { ProjectContents } from '../core/shared/project-file-types'
4-
import { directory, isDirectory } from '../core/shared/project-file-types'
4+
import {
5+
directory,
6+
isDirectory,
7+
RevisionsState,
8+
textFile,
9+
textFileContents,
10+
unparsed,
11+
} from '../core/shared/project-file-types'
512
import { codeFile } from '../core/shared/project-file-types'
613
import type { ProjectContentTreeRoot } from './assets'
714
import {
815
contentsToTree,
16+
getProjectDependencies,
917
projectContentDirectory,
1018
projectContentFile,
1119
treeToContents,
1220
} from './assets'
1321
import { projectContentsArbitrary } from './assets.test-utils'
22+
import { simpleDefaultProject } from '../sample-projects/sample-project-utils'
1423

1524
function checkContentsToTree(contents: ProjectContents): boolean {
1625
const result = treeToContents(contentsToTree(contents))
@@ -51,3 +60,37 @@ describe('contentsToTree', () => {
5160
expect(contentsToTree(contents)).toEqual(expectedResult)
5261
})
5362
})
63+
64+
describe('getProjectDependencies', () => {
65+
it('should merge the dependencies from package.json and package-lock.json', () => {
66+
const project = simpleDefaultProject({
67+
additionalFiles: {
68+
'/package-lock.json': textFile(
69+
textFileContents(
70+
JSON.stringify(
71+
{
72+
dependencies: {
73+
react: {
74+
version: '1.0.0',
75+
},
76+
},
77+
},
78+
null,
79+
2,
80+
),
81+
unparsed,
82+
RevisionsState.CodeAhead,
83+
),
84+
null,
85+
null,
86+
0,
87+
),
88+
},
89+
})
90+
const dependencies = getProjectDependencies(project.projectContents)
91+
// from package.json
92+
expect(dependencies?.['react-dom']).toEqual('16.13.1')
93+
// from package-lock.json
94+
expect(dependencies?.react).toEqual('1.0.0')
95+
})
96+
})

editor/src/components/assets.ts

+33
Original file line numberDiff line numberDiff line change
@@ -761,3 +761,36 @@ export function ensureDirectoriesExist(projectContents: ProjectContents): Projec
761761
return result
762762
}
763763
}
764+
765+
function getFileAsJson<T>(projectContents: ProjectContentTreeRoot, fileName: string): T | null {
766+
const file = getProjectFileByFilePath(projectContents, fileName)
767+
if (file != null && isTextFile(file)) {
768+
return JSON.parse(file.fileContents.code) as T
769+
}
770+
return null
771+
}
772+
773+
type PackageJson = { utopia?: Record<string, string>; dependencies?: Record<string, string> }
774+
export function getPackageJson(projectContents: ProjectContentTreeRoot): PackageJson | null {
775+
return getFileAsJson<PackageJson>(projectContents, '/package.json')
776+
}
777+
778+
type PackageLockJson = { dependencies?: Record<string, { version?: string }> }
779+
export function getPackageLockJson(
780+
projectContents: ProjectContentTreeRoot,
781+
): PackageLockJson | null {
782+
return getFileAsJson<PackageLockJson>(projectContents, '/package-lock.json')
783+
}
784+
785+
export function getProjectDependencies(
786+
projectContents: ProjectContentTreeRoot,
787+
): Record<string, string> | null {
788+
const packageJsonDependencies = getPackageJson(projectContents)?.dependencies ?? {}
789+
const packageLockJsonDependencies = getPackageLockJson(projectContents)?.dependencies ?? {}
790+
for (const packageName of Object.keys(packageJsonDependencies)) {
791+
if (packageLockJsonDependencies[packageName]?.version != null) {
792+
packageJsonDependencies[packageName] = packageLockJsonDependencies[packageName].version
793+
}
794+
}
795+
return packageJsonDependencies
796+
}

editor/src/components/editor/import-wizard/components.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ const dependenciesSuccessFn = (op: ImportFetchDependency) =>
102102
const dependenciesSuccessTextFn = (successCount: number) =>
103103
`${successCount} dependencies fetched successfully`
104104
const requirementsSuccessFn = (op: ImportCheckRequirementAndFix) =>
105-
op.resolution === RequirementResolutionResult.Found
105+
op.resolution === RequirementResolutionResult.Passed
106106
const requirementsSuccessTextFn = (successCount: number) => `${successCount} requirements met`
107107

108108
function AggregatedChildrenStatus<T extends ImportOperation>({

editor/src/components/editor/store/store-deep-equality-instances.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -4802,7 +4802,7 @@ export const ProjectRequirementResolutionKeepDeepEquality: KeepDeepEqualityCall<
48024802
)
48034803

48044804
export const ProjectRequirementsKeepDeepEquality: KeepDeepEqualityCall<ProjectRequirements> =
4805-
combine4EqualityCalls(
4805+
combine5EqualityCalls(
48064806
(requirements) => requirements.storyboard,
48074807
ProjectRequirementResolutionKeepDeepEquality,
48084808
(requirements) => requirements.packageJsonEntries,
@@ -4811,6 +4811,8 @@ export const ProjectRequirementsKeepDeepEquality: KeepDeepEqualityCall<ProjectRe
48114811
ProjectRequirementResolutionKeepDeepEquality,
48124812
(requirements) => requirements.reactVersion,
48134813
ProjectRequirementResolutionKeepDeepEquality,
4814+
(requirements) => requirements.serverPackages,
4815+
ProjectRequirementResolutionKeepDeepEquality,
48144816
newProjectRequirements,
48154817
)
48164818

Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1-
import { getProjectFileByFilePath } from '../../../../components/assets'
21
import type { ProjectContentTreeRoot } from 'utopia-shared/src/types'
3-
import { isTextFile } from '../../project-file-types'
42
import type { EditorDispatch } from '../../../../components/editor/action-types'
53
import CheckPackageJson from './requirements/requirement-package-json'
64
import CheckLanguage from './requirements/requirement-language'
75
import CheckReactVersion from './requirements/requirement-react'
86
import { RequirementResolutionResult } from './utopia-requirements-types'
97
import type { ProjectRequirement, RequirementCheck } from './utopia-requirements-types'
10-
import { notifyCheckingRequirement, notifyResolveRequirement } from './utopia-requirements-service'
8+
import {
9+
initialTexts,
10+
notifyCheckingRequirement,
11+
notifyResolveRequirement,
12+
} from './utopia-requirements-service'
1113
import CheckStoryboard from './requirements/requirement-storyboard'
14+
import CheckServerPackages from './requirements/requirement-server-packages'
1215

1316
export function checkAndFixUtopiaRequirements(
1417
dispatch: EditorDispatch,
@@ -19,13 +22,14 @@ export function checkAndFixUtopiaRequirements(
1922
packageJsonEntries: new CheckPackageJson(),
2023
language: new CheckLanguage(),
2124
reactVersion: new CheckReactVersion(),
25+
serverPackages: new CheckServerPackages(),
2226
}
2327
let projectContents = parsedProjectContents
2428
let result: RequirementResolutionResult = RequirementResolutionResult.Found
2529
// iterate over all checks, updating the project contents as we go
2630
for (const [name, check] of Object.entries(checks)) {
2731
const checkName = name as ProjectRequirement
28-
notifyCheckingRequirement(dispatch, checkName, check.getStartText())
32+
notifyCheckingRequirement(dispatch, checkName, initialTexts[checkName])
2933
const checkResult = check.check(projectContents)
3034
if (checkResult.resolution === RequirementResolutionResult.Critical) {
3135
result = RequirementResolutionResult.Critical
@@ -41,29 +45,3 @@ export function checkAndFixUtopiaRequirements(
4145
}
4246
return { result: result, fixedProjectContents: projectContents }
4347
}
44-
45-
export function getPackageJson(
46-
projectContents: ProjectContentTreeRoot,
47-
): { utopia?: Record<string, string>; dependencies?: Record<string, string> } | null {
48-
return getJsonFile<{ utopia?: Record<string, string>; dependencies?: Record<string, string> }>(
49-
projectContents,
50-
'/package.json',
51-
)
52-
}
53-
54-
export function getPackageLockJson(
55-
projectContents: ProjectContentTreeRoot,
56-
): { dependencies?: Record<string, string> } | null {
57-
return getJsonFile<{ dependencies?: Record<string, string> }>(
58-
projectContents,
59-
'/package-lock.json',
60-
)
61-
}
62-
63-
function getJsonFile<T>(projectContents: ProjectContentTreeRoot, fileName: string): T | null {
64-
const file = getProjectFileByFilePath(projectContents, fileName)
65-
if (file != null && isTextFile(file)) {
66-
return JSON.parse(file.fileContents.code) as T
67-
}
68-
return null
69-
}

editor/src/core/shared/import/proejct-health-check/requirements/requirement-language.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@ import {
77
import { applyToAllUIJSFiles } from '../../../../model/project-file-utils'
88

99
export default class CheckProjectLanguage implements RequirementCheck {
10-
getStartText(): string {
11-
return 'Checking project language'
12-
}
1310
check(projectContents: ProjectContentTreeRoot): RequirementCheckResult {
1411
let jsCount = 0
1512
let tsCount = 0
@@ -35,7 +32,7 @@ export default class CheckProjectLanguage implements RequirementCheck {
3532
}
3633
} else {
3734
return {
38-
resolution: RequirementResolutionResult.Found,
35+
resolution: RequirementResolutionResult.Passed,
3936
resultText: 'Project uses JS/JSX',
4037
resultValue: 'javascript',
4138
}

editor/src/core/shared/import/proejct-health-check/requirements/requirement-package-json.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
import type { ProjectContentTreeRoot } from 'utopia-shared/src/types'
22
import { RevisionsState } from 'utopia-shared/src/types'
3-
import { getPackageJson } from '../check-utopia-requirements'
3+
import { getPackageJson } from '../../../../../components/assets'
44
import type { RequirementCheck, RequirementCheckResult } from '../utopia-requirements-types'
55
import { RequirementResolutionResult } from '../utopia-requirements-types'
66
import { addFileToProjectContents } from '../../../../../components/assets'
77
import { codeFile } from '../../../../../core/shared/project-file-types'
88

99
export default class PackageJsonCheckAndFix implements RequirementCheck {
10-
getStartText(): string {
11-
return 'Checking package.json'
12-
}
1310
check(projectContents: ProjectContentTreeRoot): RequirementCheckResult {
1411
const parsedPackageJson = getPackageJson(projectContents)
1512
if (parsedPackageJson == null) {
@@ -39,7 +36,7 @@ export default class PackageJsonCheckAndFix implements RequirementCheck {
3936
}
4037
} else {
4138
return {
42-
resolution: RequirementResolutionResult.Found,
39+
resolution: RequirementResolutionResult.Passed,
4340
resultText: 'Valid package.json found',
4441
}
4542
}

editor/src/core/shared/import/proejct-health-check/requirements/requirement-react.ts

+4-13
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,15 @@ import {
44
type RequirementCheck,
55
type RequirementCheckResult,
66
} from '../utopia-requirements-types'
7-
import { getPackageJson, getPackageLockJson } from '../check-utopia-requirements'
87
import Semver from 'semver'
8+
import { getProjectDependencies } from '../../../../../components/assets'
99

1010
const SUPPORTED_REACT_VERSION_RANGE = '16.8.0 - 18.x'
1111

1212
export default class CheckReactRequirement implements RequirementCheck {
13-
getStartText(): string {
14-
return 'Checking React version'
15-
}
1613
check(projectContents: ProjectContentTreeRoot): RequirementCheckResult {
17-
const parsedPackageLockJson = getPackageLockJson(projectContents)
18-
// check package-lock.json first
19-
let reactVersion = parsedPackageLockJson?.dependencies?.react
20-
if (reactVersion == null) {
21-
const parsedPackageJson = getPackageJson(projectContents)
22-
// then check package.json
23-
reactVersion = parsedPackageJson?.dependencies?.react
24-
}
14+
const projectDependencies = getProjectDependencies(projectContents)
15+
const reactVersion = projectDependencies?.react
2516
if (reactVersion == null) {
2617
return {
2718
resolution: RequirementResolutionResult.Critical,
@@ -31,7 +22,7 @@ export default class CheckReactRequirement implements RequirementCheck {
3122
const isMatching = Semver.intersects(reactVersion, SUPPORTED_REACT_VERSION_RANGE)
3223
return {
3324
resolution: isMatching
34-
? RequirementResolutionResult.Found
25+
? RequirementResolutionResult.Passed
3526
: RequirementResolutionResult.Critical,
3627
resultText: isMatching ? 'React version is ok' : 'React version is not in supported range',
3728
resultValue: reactVersion,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { ProjectContentTreeRoot } from 'utopia-shared/src/types'
2+
import {
3+
RequirementResolutionResult,
4+
type RequirementCheck,
5+
type RequirementCheckResult,
6+
} from '../utopia-requirements-types'
7+
import { getProjectDependencies } from '../../../../../components/assets'
8+
9+
const serverPackagesRestrictionList: RegExp[] = [/^next/, /^remix/, /^astro/, /^svelte/]
10+
11+
export default class CheckServerPackages implements RequirementCheck {
12+
check(projectContents: ProjectContentTreeRoot): RequirementCheckResult {
13+
const projectDependencies = getProjectDependencies(projectContents) ?? {}
14+
const serverPackages = Object.keys(projectDependencies).filter((packageName) =>
15+
serverPackagesRestrictionList.some((restriction) => restriction.test(packageName)),
16+
)
17+
if (serverPackages.length > 0) {
18+
return {
19+
resolution: RequirementResolutionResult.Critical,
20+
resultText: 'Server packages found',
21+
resultValue: serverPackages.join(', '),
22+
}
23+
}
24+
return {
25+
resolution: RequirementResolutionResult.Passed,
26+
resultText: 'No server packages found',
27+
}
28+
}
29+
}

editor/src/core/shared/import/proejct-health-check/requirements/requirement-storyboard.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@ import { codeFile } from '../../../../../core/shared/project-file-types'
1313
import { addStoryboardFileToProject } from '../../../../../core/model/storyboard-utils'
1414

1515
export default class CheckStoryboard implements RequirementCheck {
16-
getStartText(): string {
17-
return 'Checking for storyboard.js'
18-
}
1916
check(projectContents: ProjectContentTreeRoot): RequirementCheckResult {
2017
return createStoryboardFileIfNecessaryInner(projectContents)
2118
}
@@ -34,7 +31,7 @@ function createStoryboardFileIfNecessaryInner(
3431
const storyboardFile = getProjectFileByFilePath(projectContents, StoryboardFilePath)
3532
if (storyboardFile != null) {
3633
return {
37-
resolution: RequirementResolutionResult.Found,
34+
resolution: RequirementResolutionResult.Passed,
3835
resultText: 'Storyboard.js found',
3936
}
4037
}

0 commit comments

Comments
 (0)