forked from quasarframework/quasar
-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
InstallAPI.js
311 lines (277 loc) · 8.86 KB
/
InstallAPI.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
const fs = require('fs-extra')
const path = require('path')
const merge = require('webpack-merge')
const semver = require('semver')
const appPaths = require('../app-paths')
const { warn, fatal } = require('../helpers/logger')
const getPackageJson = require('../helpers/get-package-json')
const getCallerPath = require('../helpers/get-caller-path')
const extensionJson = require('./extension-json')
/**
* API for extension's /install.js script
*/
module.exports = class InstallAPI {
constructor ({ extId, prompts }) {
this.extId = extId
this.prompts = prompts
this.resolve = appPaths.resolve
this.appDir = appPaths.appDir
this.__needsNodeModulesUpdate = false
this.__hooks = {
renderFolders: [],
renderFiles: [],
exitLog: []
}
}
/**
* Get the internal persistent config of this extension.
* Returns empty object if it has none.
*
* @return {object} cfg
*/
getPersistentConf () {
return extensionJson.getInternal(this.extId)
}
/**
* Set the internal persistent config of this extension.
* If it already exists, it is overwritten.
*
* @param {object} cfg
*/
setPersistentConf (cfg) {
extensionJson.setInternal(this.extId, cfg || {})
}
/**
* Deep merge into the internal persistent config of this extension.
* If extension does not have any config already set, this is
* essentially equivalent to setting it for the first time.
*
* @param {object} cfg
*/
mergePersistentConf (cfg = {}) {
const currentCfg = this.getPersistentConf()
this.setPersistentConf(merge(currentCfg, cfg))
}
/**
* Ensure the App Extension is compatible with
* host app installed package through a
* semver condition.
*
* If the semver condition is not met, then
* @quasar/app errors out and halts execution
*
* Example of semver condition:
* '1.x || >=2.5.0 || 5.0.0 - 7.2.3'
*
* @param {string} packageName
* @param {string} semverCondition
*/
compatibleWith (packageName, semverCondition) {
const json = getPackageJson(packageName)
if (json === void 0) {
fatal(`Extension(${this.extId}): Dependency not found - ${packageName}. Please install it.`)
}
if (!semver.satisfies(json.version, semverCondition)) {
fatal(`Extension(${this.extId}): is not compatible with ${packageName} v${json.version}. Required version: ${semverCondition}`)
}
}
/**
* Check if an app package is installed. Can also
* check its version against specific semver condition.
*
* Example of semver condition:
* '1.x || >=2.5.0 || 5.0.0 - 7.2.3'
*
* @param {string} packageName
* @param {string} (optional) semverCondition
* @return {boolean} package is installed and meets optional semver condition
*/
hasPackage (packageName, semverCondition) {
const json = getPackageJson(packageName)
if (json === void 0) {
return false
}
return semverCondition !== void 0
? semver.satisfies(json.version, semverCondition)
: true
}
/**
* Check if another app extension is installed
* (app extension npm package is installed and it was invoked)
*
* @param {string} extId
* @return {boolean} has the extension installed & invoked
*/
hasExtension (extId) {
return extensionJson.has(extId)
}
/**
* Get the version of an an app's package.
*
* @param {string} packageName
* @return {string|undefined} version of app's package
*/
getPackageVersion (packageName) {
const json = getPackageJson(packageName)
return json !== void 0
? json.version
: void 0
}
/**
* Extend package.json with new props.
* If specifying existing props, it will override them.
*
* @param {object|string} extPkg - Object to extend with or relative path to a JSON file
*/
extendPackageJson (extPkg) {
if (!extPkg) {
return
}
if (typeof extPkg === 'string') {
const dir = getCallerPath()
const source = path.resolve(dir, extPkg)
if (!fs.existsSync(source)) {
warn()
warn(`Extension(${this.extId}): extendPackageJson() - cannot locate ${extPkg}. Skipping...`)
warn()
return
}
if (fs.lstatSync(source).isDirectory()) {
warn()
warn(`Extension(${this.extId}): extendPackageJson() - "${extPkg}" is a folder instead of file. Skipping...`)
warn()
return
}
try {
extPkg = require(source)
}
catch (e) {
warn(`Extension(${this.extId}): extendPackageJson() - "${extPkg}" is malformed`)
warn()
process.exit(1)
}
}
if (Object(extPkg) !== extPkg || Object.keys(extPkg).length === 0) {
return
}
const filePath = appPaths.resolve.app('package.json')
const pkg = merge(require(filePath), extPkg)
fs.writeFileSync(
filePath,
JSON.stringify(pkg, null, 2),
'utf-8'
)
if (
extPkg.dependencies ||
extPkg.devDependencies ||
extPkg.optionalDependencies ||
extPkg.bundleDependencies ||
extPkg.peerDependencies
) {
this.__needsNodeModulesUpdate = true
}
}
/**
* Extend a JSON file with new props (deep merge).
* If specifying existing props, it will override them.
*
* @param {string} file (relative path to app root folder)
* @param {object} newData (Object to merge in)
*/
extendJsonFile (file, newData) {
if (newData !== void 0 && Object(newData) === newData && Object.keys(newData).length > 0) {
const filePath = appPaths.resolve.app(file)
// Try to parse the JSON with Node native tools.
// It will soft-fail and log a warning if the JSON isn't parseable
// which usually means we are dealing with an extended JSON flavour,
// for example JSON with comments or JSON5.
// Notable examples are TS 'tsconfig.json' or VSCode 'settings.json'
try {
const data = merge(fs.existsSync(filePath) ? require(filePath) : {}, newData)
fs.writeFileSync(
appPaths.resolve.app(file),
JSON.stringify(data, null, 2),
'utf-8'
)
}
catch(e) {
warn()
warn(`Extension(${this.extId}): extendJsonFile() - "${filePath}" doesn't conform to JSON format: this could happen if you are trying to update flavoured JSON files (eg. JSON with Comments or JSON5). Skipping...`)
warn(`Extension(${this.extId}): extendJsonFile() - The extension tried to apply these updates to "${filePath}" file: ${JSON.stringify(newData)}`)
warn()
}
}
}
/**
* Render a folder from extension templates into devland.
* Needs a path (to a folder) relative to the path of the file where render() is called
*
* @param {string} templatePath (relative path to folder to render in app)
* @param {object} scope (optional; rendering scope variables)
*/
render (templatePath, scope) {
const dir = getCallerPath()
const source = path.resolve(dir, templatePath)
const rawCopy = !scope || Object.keys(scope).length === 0
if (!fs.existsSync(source)) {
warn()
warn(`Extension(${this.extId}): render() - cannot locate ${templatePath}. Skipping...\n`)
return
}
if (!fs.lstatSync(source).isDirectory()) {
warn()
warn(`Extension(${this.extId}): render() - "${templatePath}" is a file instead of folder. Skipping...\n`)
return
}
this.__hooks.renderFolders.push({
source,
rawCopy,
scope
})
}
/**
* Render a file from extension template into devland
* Needs a path (to a file) relative to the path of the file where renderFile() is called
*
* @param {string} relativeSourcePath (file path relative to the folder from which the install script is called)
* @param {string} relativeTargetPath (file path relative to the root of the app -- including filename!)
* @param {object} scope (optional; rendering scope variables)
*/
renderFile (relativeSourcePath, relativeTargetPath, scope) {
const dir = getCallerPath()
const sourcePath = path.resolve(dir, relativeSourcePath)
const targetPath = appPaths.resolve.app(relativeTargetPath)
const rawCopy = !scope || Object.keys(scope).length === 0
if (!fs.existsSync(sourcePath)) {
warn()
warn(`Extension(${this.extId}): renderFile() - cannot locate ${relativeSourcePath}. Skipping...\n`)
return
}
if (fs.lstatSync(sourcePath).isDirectory()) {
warn()
warn(`Extension(${this.extId}): renderFile() - "${relativeSourcePath}" is a folder instead of a file. Skipping...\n`)
return
}
this.__hooks.renderFiles.push({
sourcePath,
targetPath,
rawCopy,
scope,
overwritePrompt: true
})
}
/**
* Add a message to be printed after App CLI finishes up install.
*
* @param {string} msg
*/
onExitLog (msg) {
this.__hooks.exitLog.push(msg)
}
/**
* Private methods
*/
__getHooks () {
return this.__hooks
}
}