Skip to content
This repository was archived by the owner on Jan 11, 2023. It is now read-only.

Commit 8f4c78a

Browse files
authoredOct 27, 2022
feat: detect installed version of framework (#822)
* feat: (WIP) add framework version detection * feat: include version of installed framework in listFrameworks response * chore: upgrade node version to use to 14 * test: add mock installed package * test: add test case for multiple frameworks * fix: update import path * 9.3.1-framework-version-detection.0 * feat: support monorepo case * 9.3.1-framework-version-detection.1 * refactor: general cleanup before starting code review
1 parent 6e19105 commit 8f4c78a

File tree

17 files changed

+1056
-266
lines changed

17 files changed

+1056
-266
lines changed
 

‎.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,8 @@ cypress/support
1212

1313
# Local Netlify folder
1414
.netlify
15+
16+
# These are mock node_modules folder for testing purposes
17+
!test/fixtures/simple/node_modules
18+
!test/fixtures/multiple/node_modules
19+
!test/fixtures/monorepos/node_modules

‎package-lock.json

+804-245
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+6-2
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,16 @@
7171
"dependencies": {
7272
"ajv": "^8.0.0",
7373
"filter-obj": "^3.0.0",
74+
"find-up": "^6.3.0",
75+
"fs-extra": "^10.1.0",
7476
"is-plain-obj": "^4.0.0",
7577
"locate-path": "^7.0.0",
7678
"p-filter": "^3.0.0",
7779
"p-locate": "^6.0.0",
80+
"process": "^0.11.10",
7881
"read-pkg-up": "^9.0.0",
79-
"semver": "^7.3.4"
82+
"semver": "^7.3.4",
83+
"url": "^0.11.0"
8084
},
8185
"devDependencies": {
8286
"@netlify/eslint-config-node": "^6.0.0",
@@ -94,7 +98,7 @@
9498
"vite": "^3.1.6"
9599
},
96100
"engines": {
97-
"node": "^12.20.0 || ^14.14.0 || >=16.0.0"
101+
"node": "^14.14.0 || >=16.0.0"
98102
},
99103
"ava": {
100104
"verbose": true,

‎src/context.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,29 @@
1-
import { cwd, version } from 'process'
1+
import { cwd, version as nodejsVersion } from 'process'
22

33
import isPlainObj from 'is-plain-obj'
44
import { locatePath } from 'locate-path'
55
import { readPackageUp } from 'read-pkg-up'
66

7-
const getPackageJson = async (projectDir) => {
7+
export const getPackageJson = async (projectDir) => {
88
try {
99
const result = await readPackageUp({ cwd: projectDir, normalize: false })
1010
if (result === undefined) {
1111
return {}
1212
}
1313

14-
const { packageJson, path: packageJsonPath } = result
14+
const { version, packageJson, path: packageJsonPath } = result
1515

1616
if (!isPlainObj(packageJson)) {
1717
return { packageJsonPath }
1818
}
1919

20-
return { packageJson, packageJsonPath }
20+
return { version, packageJson, packageJsonPath }
2121
} catch {
2222
return {}
2323
}
2424
}
2525

26-
export const getContext = async ({ projectDir = cwd(), nodeVersion = version } = {}) => {
26+
export const getContext = async ({ projectDir = cwd(), nodeVersion = nodejsVersion } = {}) => {
2727
const { packageJson, packageJsonPath = projectDir } = await getPackageJson(projectDir)
2828
return {
2929
pathExists: async (path) => (await locatePath([path], { type: 'file', cwd: projectDir })) !== undefined,

‎src/core.js

+5
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ const getFrameworkInfo = function (
142142
{
143143
id,
144144
name,
145+
detect,
145146
category,
146147
dev: { command: frameworkDevCommand, port, pollingStrategies },
147148
build: { command: frameworkBuildCommand, directory },
@@ -157,6 +158,10 @@ const getFrameworkInfo = function (
157158
return {
158159
id,
159160
name,
161+
package: {
162+
name: detect.npmDependencies[0],
163+
version: 'unknown',
164+
},
160165
category,
161166
dev: { commands: devCommands, port, pollingStrategies },
162167
build: { commands: [frameworkBuildCommand], directory },

‎src/main.js

+62-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { getContext } from './context.js'
1+
import { join } from 'path'
2+
import { cwd, chdir } from 'process'
3+
4+
import { findUp } from 'find-up'
5+
6+
import { getContext, getPackageJson } from './context.js'
27
import { listFrameworks as list, hasFramework as has, getFramework as get } from './core.js'
38

49
/**
@@ -19,16 +24,62 @@ import { listFrameworks as list, hasFramework as has, getFramework as get } from
1924
* @property {string} directory - Relative path to the directory where files are built
2025
*/
2126

27+
/**
28+
* @typedef {object} FrameworkPackage
29+
* @property {string} name - The name of the package. e.g: 'gatsby'
30+
* @property {string} version - The version of the installed package as found in the package.json. Is set to 'unknown' by default.
31+
*/
32+
2233
/**
2334
* @typedef {object} Framework
2435
* @property {string} id - Id such as `"gatsby"`
2536
* @property {string} category - Category among `"static_site_generator"`, `"frontend_framework"` and `"build_tool"`
37+
* @property {FrameworkPackage} package - Information about the framework's underlying package
2638
* @property {Dev} dev - Information about the dev command
2739
* @property {Build} build - Information about the build command
2840
* @property {object} env - Environment variables that should be set when calling the dev command
2941
* @property {string[]} plugins - A list of recommend Netlify build plugins to install for the framework
3042
*/
3143

44+
/**
45+
* Gets the version of the framework that is installed in a project.
46+
*
47+
* This cannot currently be used in the browser at this time, which is why it's defined
48+
* here rather than in `core.js` as part of the `getFrameworkInfo` method
49+
*
50+
* @param {string} projectDir - Project directory
51+
* @param {Framework} frameworkInfo - Information about the framework as detected by `getFrameworkInfo`
52+
*
53+
* @returns {Promise<Framework>}
54+
*/
55+
const getFrameworkVersion = async (projectDir, frameworkInfo) => {
56+
// Need to change the CWD to the project directory in order to make sure we find and use the correct
57+
// package.json
58+
const originalCwd = cwd()
59+
const returnToOriginalDirectory = () => {
60+
chdir(originalCwd)
61+
}
62+
chdir(projectDir)
63+
64+
const npmPackage = frameworkInfo.package.name
65+
66+
// Get path of package.json for the installed framework. We need to traverse up the directories
67+
// in the event that the project uses something like npm workspaces, and the installed framework package
68+
// has been hoisted to the root directory of the project (which differs from the directory of the project/application being built)
69+
const installedFrameworkPath = await findUp(join('node_modules', npmPackage, 'package.json'))
70+
const { packageJson } = await getPackageJson(installedFrameworkPath)
71+
72+
returnToOriginalDirectory()
73+
74+
return {
75+
...frameworkInfo,
76+
package: {
77+
name: frameworkInfo.package.name,
78+
version: packageJson.version || 'unknown',
79+
},
80+
}
81+
}
82+
3283
/**
3384
* Return all the frameworks used by a project.
3485
*
@@ -38,7 +89,16 @@ import { listFrameworks as list, hasFramework as has, getFramework as get } from
3889
*/
3990
export const listFrameworks = async function (opts) {
4091
const context = await getContext(opts)
41-
return await list(context)
92+
const frameworkList = await list(context)
93+
94+
const projectDir = opts && opts.projectDir ? opts.projectDir : cwd()
95+
96+
const settledPromises = await Promise.allSettled(
97+
frameworkList.map((framework) => getFrameworkVersion(projectDir, framework)),
98+
)
99+
const updatedList = settledPromises.map((result) => result.value)
100+
101+
return updatedList
42102
}
43103

44104
/**

‎test/dev.js

-11
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,6 @@ test('Should default dev.commands to framework.dev.command', async (t) => {
2626
t.deepEqual(frameworks[0].dev.commands, ['sapper dev'])
2727
})
2828

29-
test('Should return the same result when script order is different', async (t) => {
30-
const frameworksBuildFirst = await getFrameworks('scripts-order/build-first')
31-
const frameworksDevFirst = await getFrameworks('scripts-order/dev-first')
32-
33-
t.is(frameworksBuildFirst.length, 1)
34-
t.is(frameworksDevFirst.length, 1)
35-
36-
t.deepEqual(frameworksBuildFirst[0].dev.commands, ['npm run dev', 'npm run start', 'npm run build'])
37-
t.deepEqual(frameworksBuildFirst, frameworksDevFirst)
38-
})
39-
4029
test('Should sort scripts in the format *:<name>', async (t) => {
4130
const frameworks = await getFrameworks('scripts-order/postfix-format')
4231

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "app1",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
"next":"*"
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "app2",
3+
"version": "2.0.0",
4+
"dependencies": {
5+
"next":"*"
6+
}
7+
}

‎test/fixtures/monorepos/node_modules/next/package.json

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎test/fixtures/multiple/node_modules/@vue/cli-service/package.json

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎test/fixtures/multiple/node_modules/vuepress/package.json

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎test/fixtures/simple/node_modules/sapper/package.json

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎test/main.js

+10
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,16 @@ test('Should return several items when multiple frameworks are detected', async
1717
t.is(frameworks.length, 2)
1818
})
1919

20+
test('Should return the version of each framework when multiple are detected', async (t) => {
21+
const frameworks = await getFrameworks('multiple')
22+
t.snapshot(frameworks)
23+
})
24+
25+
test('Should return the version of the framework when the installed package is hoisted to the root project directory', async (t) => {
26+
const frameworks = await getFrameworks('monorepos/app1')
27+
t.snapshot(frameworks)
28+
})
29+
2030
test('Should allow getting a specific framework', async (t) => {
2131
const framework = await getFramework('simple', 'sapper')
2232
t.snapshot(framework)

‎test/snapshots/main.js.md

+132
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,49 @@ The actual snapshot is saved in `main.js.snap`.
44

55
Generated by [AVA](https://avajs.dev).
66

7+
## Should return the version of the framework when the installed package is hoisted to the root project directory
8+
9+
> Snapshot 1
10+
11+
[
12+
{
13+
build: {
14+
commands: [
15+
'next build',
16+
],
17+
directory: '.next',
18+
},
19+
category: 'static_site_generator',
20+
dev: {
21+
commands: [
22+
'next',
23+
],
24+
pollingStrategies: [
25+
{
26+
name: 'TCP',
27+
},
28+
],
29+
port: 3000,
30+
},
31+
env: {},
32+
id: 'next',
33+
logo: {
34+
dark: 'https://framework-info.netlify.app/logos/nextjs/dark.svg',
35+
default: 'https://framework-info.netlify.app/logos/nextjs/light.svg',
36+
light: 'https://framework-info.netlify.app/logos/nextjs/light.svg',
37+
},
38+
name: 'Next.js',
39+
package: {
40+
name: 'next',
41+
version: '3.2.1',
42+
},
43+
plugins: [
44+
'@netlify/plugin-nextjs',
45+
],
46+
staticAssetsDirectory: undefined,
47+
},
48+
]
49+
750
## Should detect frameworks
851

952
> Snapshot 1
@@ -39,11 +82,96 @@ Generated by [AVA](https://avajs.dev).
3982
light: 'https://framework-info.netlify.app/logos/sapper/default.svg',
4083
},
4184
name: 'Sapper',
85+
package: {
86+
name: 'sapper',
87+
version: '3.4.3',
88+
},
4289
plugins: [],
4390
staticAssetsDirectory: 'static',
4491
},
4592
]
4693

94+
## Should return the version of each framework when multiple are detected
95+
96+
> Snapshot 1
97+
98+
[
99+
{
100+
build: {
101+
commands: [
102+
'vuepress build',
103+
],
104+
directory: '.vuepress/dist',
105+
},
106+
category: 'static_site_generator',
107+
dev: {
108+
commands: [
109+
'vuepress dev',
110+
],
111+
pollingStrategies: [
112+
{
113+
name: 'TCP',
114+
},
115+
{
116+
name: 'HTTP',
117+
},
118+
],
119+
port: 8080,
120+
},
121+
env: {},
122+
id: 'vuepress',
123+
logo: {
124+
dark: 'https://framework-info.netlify.app/logos/vuepress/default.svg',
125+
default: 'https://framework-info.netlify.app/logos/vuepress/default.svg',
126+
light: 'https://framework-info.netlify.app/logos/vuepress/default.svg',
127+
},
128+
name: 'VuePress',
129+
package: {
130+
name: 'vuepress',
131+
version: '4.5.6',
132+
},
133+
plugins: [],
134+
staticAssetsDirectory: undefined,
135+
},
136+
{
137+
build: {
138+
commands: [
139+
'vue-cli-service build',
140+
],
141+
directory: 'dist',
142+
},
143+
category: 'frontend_framework',
144+
dev: {
145+
commands: [
146+
'vue-cli-service serve',
147+
],
148+
pollingStrategies: [
149+
{
150+
name: 'TCP',
151+
},
152+
{
153+
name: 'HTTP',
154+
},
155+
],
156+
port: 8080,
157+
},
158+
env: {},
159+
id: 'vue',
160+
logo: {
161+
dark: 'https://framework-info.netlify.app/logos/vue/default.svg',
162+
default: 'https://framework-info.netlify.app/logos/vue/default.svg',
163+
light: 'https://framework-info.netlify.app/logos/vue/default.svg',
164+
},
165+
name: 'Vue.js',
166+
package: {
167+
name: '@vue/cli-service',
168+
version: '1.2.3',
169+
},
170+
plugins: [],
171+
staticAssetsDirectory: undefined,
172+
},
173+
]
174+
47175
## Should allow getting a specific framework
48176

49177
> Snapshot 1
@@ -78,6 +206,10 @@ Generated by [AVA](https://avajs.dev).
78206
light: 'https://framework-info.netlify.app/logos/sapper/default.svg',
79207
},
80208
name: 'Sapper',
209+
package: {
210+
name: 'sapper',
211+
version: 'unknown',
212+
},
81213
plugins: [],
82214
staticAssetsDirectory: 'static',
83215
}

‎test/snapshots/main.js.snap

987 Bytes
Binary file not shown.

‎vite.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { fileURLToPath } from 'url'
1+
import { fileURLToPath } from 'node:url'
22
import nodePolyfills from 'rollup-plugin-node-polyfills'
33

44
const CORE_FILE = fileURLToPath(new URL('src/core.js', import.meta.url))

0 commit comments

Comments
 (0)
This repository has been archived.