Skip to content

Commit 938adbd

Browse files
authored
fix: honor forced framework for Remix and Hydrogen (#5837)
* test: add check to see if all the frameworks honors forced framework * fix: actually merge detections and not just pick up the most accurate one * refactor: use single detection pass with remix and hydrogen * fix: honor forced remix and hydrogen framework and allow detecting when config file is missing
1 parent 413b127 commit 938adbd

File tree

7 files changed

+96
-62
lines changed

7 files changed

+96
-62
lines changed

packages/build-info/src/frameworks/framework.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,26 @@ describe('detection merging', () => {
205205
]),
206206
).toMatchObject({ accuracy: Accuracy.NPM, package: { name: 'a' } })
207207
})
208+
209+
test('keeps information about config and dependency from the most accurate detection that has it', () => {
210+
expect(
211+
mergeDetections([
212+
undefined,
213+
{ accuracy: Accuracy.Config, config: '/absolute/path/config.js', configName: 'config.js' },
214+
{ accuracy: Accuracy.NPMHoisted, package: { name: 'b' } },
215+
{ accuracy: Accuracy.Forced },
216+
{ accuracy: Accuracy.NPM, package: { name: 'a' } },
217+
]),
218+
).toMatchObject({
219+
// set by highest accuracy detection
220+
accuracy: Accuracy.Forced,
221+
// provided by the NPM detection - preferred over package provided by NPMHoisted
222+
package: { name: 'a' },
223+
// provided by the Config detection
224+
config: '/absolute/path/config.js',
225+
configName: 'config.js',
226+
})
227+
})
208228
})
209229

210230
describe('framework sorting based on accuracy and type', () => {

packages/build-info/src/frameworks/framework.ts

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ export type Detection = {
3333
/** The NPM package that was able to detect it (high accuracy) */
3434
package?: { name: string; version?: SemVer }
3535
packageJSON?: PackageJson
36-
/** The config file that is associated with the framework */
36+
/** The absolute path to config file that is associated with the framework */
3737
config?: string
38+
/** The name of config file that is associated with the framework */
39+
configName?: string
3840
}
3941

4042
export type FrameworkInfo = ReturnType<Framework['toJSON']>
@@ -140,11 +142,26 @@ export function sortFrameworksBasedOnAccuracy(a: DetectedFramework, b: DetectedF
140142
return sort
141143
}
142144

143-
/** Merges a list of detection results based on accuracy to get the one with the highest accuracy */
145+
/** Merges a list of detection results based on accuracy to get the one with the highest accuracy that still contains information provided by all other detections */
144146
export function mergeDetections(detections: Array<Detection | undefined>): Detection | undefined {
145-
return detections
146-
.filter(Boolean)
147-
.sort((a: Detection, b: Detection) => (a.accuracy > b.accuracy ? -1 : a.accuracy < b.accuracy ? 1 : 0))?.[0]
147+
const definedDetections = detections
148+
.filter(function isDetection(d): d is Detection {
149+
return Boolean(d)
150+
})
151+
.sort((a: Detection, b: Detection) => (a.accuracy > b.accuracy ? -1 : a.accuracy < b.accuracy ? 1 : 0))
152+
153+
if (definedDetections.length === 0) {
154+
return
155+
}
156+
157+
return definedDetections.slice(1).reduce((merged, detection) => {
158+
merged.config = merged.config ?? detection.config
159+
merged.configName = merged.configName ?? detection.configName
160+
merged.package = merged.package ?? detection.package
161+
merged.packageJSON = merged.packageJSON ?? detection.packageJSON
162+
163+
return merged
164+
}, definedDetections[0])
148165
}
149166

150167
export abstract class BaseFramework implements Framework {
@@ -277,6 +294,7 @@ export abstract class BaseFramework implements Framework {
277294
// otherwise the npm dependency should have already triggered the detection
278295
accuracy: this.npmDependencies.length === 0 ? Accuracy.ConfigOnly : Accuracy.Config,
279296
config,
297+
configName: this.project.fs.basename(config),
280298
}
281299
}
282300
}
@@ -289,14 +307,14 @@ export abstract class BaseFramework implements Framework {
289307
* - if `configFiles` is set, one of the files must exist
290308
*/
291309
async detect(): Promise<DetectedFramework | undefined> {
292-
// we can force frameworks as well
293-
if (this.detected?.accuracy === Accuracy.Forced) {
294-
return this as DetectedFramework
295-
}
296-
297310
const npm = await this.detectNpmDependency()
298311
const config = await this.detectConfigFile(this.configFiles ?? [])
299-
this.detected = mergeDetections([npm, config])
312+
this.detected = mergeDetections([
313+
// we can force frameworks as well
314+
this.detected?.accuracy === Accuracy.Forced ? this.detected : undefined,
315+
npm,
316+
config,
317+
])
300318

301319
if (this.detected) {
302320
return this as DetectedFramework

packages/build-info/src/frameworks/hydrogen.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export class Hydrogen extends BaseFramework implements Framework {
3232
readonly id = 'hydrogen'
3333
name = 'Hydrogen'
3434
npmDependencies = ['@shopify/hydrogen']
35+
configFiles = [...VITE_CONFIG_FILES, ...CLASSIC_COMPILER_CONFIG_FILES]
3536
category = Category.SSG
3637

3738
logo = {
@@ -44,22 +45,15 @@ export class Hydrogen extends BaseFramework implements Framework {
4445
await super.detect()
4546

4647
if (this.detected) {
47-
const viteDetection = await this.detectConfigFile(VITE_CONFIG_FILES)
48-
if (viteDetection) {
49-
this.detected = viteDetection
48+
if (this.detected.configName && VITE_CONFIG_FILES.includes(this.detected.configName)) {
5049
this.dev = VITE_DEV
5150
this.build = VITE_BUILD
5251
return this as DetectedFramework
53-
}
54-
const classicCompilerDetection = await this.detectConfigFile(CLASSIC_COMPILER_CONFIG_FILES)
55-
if (classicCompilerDetection) {
56-
this.detected = classicCompilerDetection
52+
} else {
5753
this.dev = CLASSIC_COMPILER_DEV
5854
this.build = CLASSIC_COMPILER_BUILD
5955
return this as DetectedFramework
6056
}
61-
// If neither config file exists, it can't be a valid Hydrogen site for Netlify anyway.
62-
return
6357
}
6458
}
6559
}

packages/build-info/src/frameworks/remix.test.ts

Lines changed: 22 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,28 @@ beforeEach((ctx) => {
234234
}),
235235
},
236236
] as const,
237+
[
238+
'with no config file',
239+
{
240+
'package.json': JSON.stringify({
241+
scripts: {
242+
build: 'remix build',
243+
dev: 'remix dev',
244+
start: 'netlify serve',
245+
typecheck: 'tsc',
246+
},
247+
dependencies: {
248+
'@remix-run/netlify-edge': '1.7.4',
249+
remix: '1.7.4',
250+
react: '18.2.0',
251+
'react-dom': '18.2.0',
252+
},
253+
devDependencies: {
254+
'@netlify/functions': '^1.0.0',
255+
},
256+
}),
257+
},
258+
] as const,
237259
].forEach(([description, files]) =>
238260
test(`detects a Remix Classic Compiler site ${description}`, async ({ fs }) => {
239261
const cwd = mockFileSystem(files)
@@ -249,35 +271,3 @@ beforeEach((ctx) => {
249271
expect(detected?.[0]?.dev?.port).toBeUndefined()
250272
}),
251273
)
252-
253-
test('does not detect an invalid Remix site with no config file', async ({ fs }) => {
254-
const cwd = mockFileSystem({
255-
'package.json': JSON.stringify({
256-
scripts: {
257-
build: 'remix vite:build',
258-
dev: 'remix vite:dev',
259-
start: 'remix-serve ./build/server/index.js',
260-
},
261-
dependencies: {
262-
'@netlify/remix-runtime': '^2.3.0',
263-
'@remix-run/node': '^2.9.2',
264-
'@remix-run/react': '^2.9.2',
265-
'@remix-run/serve': '^2.9.2',
266-
react: '^18.2.0',
267-
'react-dom': '^18.2.0',
268-
},
269-
devDependencies: {
270-
'@netlify/remix-edge-adapter': '^3.3.0',
271-
'@remix-run/dev': '^2.9.2',
272-
'@types/react': '^18.2.20',
273-
'@types/react-dom': '^18.2.7',
274-
vite: '^5.0.0',
275-
'vite-tsconfig-paths': '^4.2.1',
276-
},
277-
}),
278-
})
279-
const detected = await new Project(fs, cwd).detectFrameworks()
280-
281-
const detectedFrameworks = (detected ?? []).map((framework) => framework.id)
282-
expect(detectedFrameworks).not.toContain('remix')
283-
})

packages/build-info/src/frameworks/remix.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export class Remix extends BaseFramework implements Framework {
4343
'@remix-run/netlify-edge',
4444
]
4545
excludedNpmDependencies = ['@shopify/hydrogen']
46+
configFiles = [...VITE_CONFIG_FILES, ...CLASSIC_COMPILER_CONFIG_FILES]
4647
category = Category.SSG
4748

4849
logo = {
@@ -55,22 +56,15 @@ export class Remix extends BaseFramework implements Framework {
5556
await super.detect()
5657

5758
if (this.detected) {
58-
const viteDetection = await this.detectConfigFile(VITE_CONFIG_FILES)
59-
if (viteDetection) {
60-
this.detected = viteDetection
59+
if (this.detected.configName && VITE_CONFIG_FILES.includes(this.detected.configName)) {
6160
this.dev = VITE_DEV
6261
this.build = VITE_BUILD
6362
return this as DetectedFramework
64-
}
65-
const classicCompilerDetection = await this.detectConfigFile(CLASSIC_COMPILER_CONFIG_FILES)
66-
if (classicCompilerDetection) {
67-
this.detected = classicCompilerDetection
63+
} else {
6864
this.dev = CLASSIC_COMPILER_DEV
6965
this.build = CLASSIC_COMPILER_BUILD
7066
return this as DetectedFramework
7167
}
72-
// If neither config file exists, it can't be a valid Remix site for Netlify anyway.
73-
return
7468
}
7569
}
7670
}

packages/build-info/src/get-framework.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { test, expect, beforeEach } from 'vitest'
1+
import { describe, test, expect, beforeEach } from 'vitest'
22

33
import { mockFileSystem } from '../tests/mock-file-system.js'
44

5+
import { frameworks } from './frameworks/index.js'
56
import { getFramework } from './get-framework.js'
67
import { NodeFS } from './node/file-system.js'
78
import { Project } from './project.js'
@@ -52,3 +53,17 @@ test('should throw if a unknown framework was requested', async ({ fs }) => {
5253
}
5354
expect.assertions(1)
5455
})
56+
57+
describe('Framework detection honors forced framework', () => {
58+
for (const Framework of frameworks) {
59+
test(Framework.name, async ({ fs }) => {
60+
const cwd = mockFileSystem({})
61+
const project = new Project(fs, cwd)
62+
const framework = new Framework(project)
63+
64+
const detectedFramework = await getFramework(framework.id, project)
65+
66+
expect(detectedFramework.id).toBe(framework.id)
67+
})
68+
}
69+
})

packages/build-info/src/get-framework.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ export async function getFramework(frameworkId: FrameworkName, project: Project)
1414
if (detected) {
1515
return detected
1616
}
17+
18+
// this indicate that framework's custom "detect" method doesn't honor the forced framework
19+
throw new Error(`Forced framework "${frameworkId}" was not detected.`)
1720
}
1821

1922
const frameworkIds = frameworkList

0 commit comments

Comments
 (0)