UNPKG

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