adapt-cli 3.0.2 → 3.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bowerrc +2 -2
- package/.eslintignore +1 -1
- package/.eslintrc.json +14 -14
- package/.github/CONTRIBUTING.md +8 -0
- package/.github/ISSUE_TEMPLATE.md +17 -0
- package/.github/pull_request_template.md +25 -0
- package/.github/workflows/addtomainproject.yml +19 -0
- package/.github/workflows/releases.yml +25 -0
- package/.travis.yml +46 -46
- package/README.md +266 -266
- package/bin/adapt.js +3 -3
- package/json/help-create/component.json +9 -9
- package/json/help-create/course.json +9 -9
- package/json/help-create/question.json +9 -9
- package/json/help-create.json +12 -12
- package/json/help-devinstall.json +9 -9
- package/json/help-install.json +10 -10
- package/json/help-ls.json +7 -7
- package/json/help-register.json +7 -7
- package/json/help-rename.json +7 -7
- package/json/help-search.json +8 -8
- package/json/help-uninstall.json +7 -7
- package/json/help-unregister.json +8 -8
- package/json/help-update.json +12 -12
- package/json/help-version.json +7 -7
- package/json/help.json +19 -19
- package/lib/api.js +260 -260
- package/lib/cli.js +69 -69
- package/lib/commands/authenticate.js +18 -18
- package/lib/commands/create/component.js +64 -64
- package/lib/commands/create/course.js +26 -26
- package/lib/commands/create/question.js +18 -18
- package/lib/commands/create.js +94 -85
- package/lib/commands/devinstall.js +35 -35
- package/lib/commands/help.js +31 -31
- package/lib/commands/install.js +16 -16
- package/lib/commands/ls.js +9 -9
- package/lib/commands/register.js +11 -11
- package/lib/commands/rename.js +14 -14
- package/lib/commands/search.js +11 -11
- package/lib/commands/uninstall.js +9 -9
- package/lib/commands/unregister.js +12 -12
- package/lib/commands/update.js +12 -12
- package/lib/commands/version.js +13 -13
- package/lib/integration/AdaptFramework/build.js +42 -42
- package/lib/integration/AdaptFramework/clone.js +27 -27
- package/lib/integration/AdaptFramework/deleteSrcCore.js +9 -9
- package/lib/integration/AdaptFramework/deleteSrcCourse.js +9 -9
- package/lib/integration/AdaptFramework/download.js +21 -21
- package/lib/integration/AdaptFramework/erase.js +34 -34
- package/lib/integration/AdaptFramework/getLatestVersion.js +79 -79
- package/lib/integration/AdaptFramework/npmInstall.js +21 -21
- package/lib/integration/AdaptFramework.js +19 -19
- package/lib/integration/Plugin.js +404 -404
- package/lib/integration/PluginManagement/autenticate.js +56 -56
- package/lib/integration/PluginManagement/install.js +224 -224
- package/lib/integration/PluginManagement/print.js +52 -52
- package/lib/integration/PluginManagement/register.js +130 -130
- package/lib/integration/PluginManagement/rename.js +101 -101
- package/lib/integration/PluginManagement/schemas.js +8 -8
- package/lib/integration/PluginManagement/search.js +46 -46
- package/lib/integration/PluginManagement/uninstall.js +141 -141
- package/lib/integration/PluginManagement/unregister.js +101 -101
- package/lib/integration/PluginManagement/update.js +224 -224
- package/lib/integration/PluginManagement.js +21 -21
- package/lib/integration/Project.js +146 -146
- package/lib/integration/Target.js +299 -299
- package/lib/integration/getBowerRegistryConfig.js +34 -34
- package/lib/logger.js +28 -28
- package/lib/util/JSONReadValidate.js +34 -34
- package/lib/util/constants.js +38 -38
- package/lib/util/createPromptTask.js +7 -7
- package/lib/util/download.js +45 -45
- package/lib/util/errors.js +58 -58
- package/lib/util/extract.js +24 -24
- package/lib/util/getDirNameFromImportMeta.js +6 -6
- package/lib/util/promises.js +36 -36
- package/package.json +74 -40
@@ -1,404 +1,404 @@
|
|
1
|
-
import slug from 'speakingurl'
|
2
|
-
import globs from 'globs'
|
3
|
-
import bower from 'bower'
|
4
|
-
import endpointParser from 'bower-endpoint-parser'
|
5
|
-
import semver from 'semver'
|
6
|
-
import fs from 'fs-extra'
|
7
|
-
import path from 'path'
|
8
|
-
import getBowerRegistryConfig from './getBowerRegistryConfig.js'
|
9
|
-
import { ADAPT_ALLOW_PRERELEASE, PLUGIN_TYPES, PLUGIN_TYPE_FOLDERS, PLUGIN_DEFAULT_TYPE } from '../util/constants.js'
|
10
|
-
/** @typedef {import("./Project.js").default} Project */
|
11
|
-
const semverOptions = { includePrerelease: ADAPT_ALLOW_PRERELEASE }
|
12
|
-
|
13
|
-
// when a bower command errors this is the maximum number of attempts the command will be repeated
|
14
|
-
const BOWER_MAX_TRY = 5
|
15
|
-
|
16
|
-
export default class Plugin {
|
17
|
-
/**
|
18
|
-
* @param {Object} options
|
19
|
-
* @param {string} options.name
|
20
|
-
* @param {string} options.requestedVersion
|
21
|
-
* @param {boolean} options.isContrib
|
22
|
-
* @param {boolean} options.isCompatibleEnabled whether to target the latest compatible version for all plugin installations (overrides requestedVersion)
|
23
|
-
* @param {Project} options.project
|
24
|
-
* @param {string} options.cwd
|
25
|
-
* @param {Object} options.logger
|
26
|
-
*/
|
27
|
-
constructor ({
|
28
|
-
name,
|
29
|
-
requestedVersion = '*',
|
30
|
-
isContrib = false,
|
31
|
-
isCompatibleEnabled = false,
|
32
|
-
project,
|
33
|
-
cwd = (project?.cwd ?? process.cwd()),
|
34
|
-
logger
|
35
|
-
} = {}) {
|
36
|
-
this.logger = logger
|
37
|
-
/** @type {Project} */
|
38
|
-
this.project = project
|
39
|
-
this.cwd = cwd
|
40
|
-
this.BOWER_REGISTRY_CONFIG = getBowerRegistryConfig({ cwd: this.cwd })
|
41
|
-
const endpoint = name + '#' + (isCompatibleEnabled ? '*' : requestedVersion)
|
42
|
-
const ep = endpointParser.decompose(endpoint)
|
43
|
-
this.sourcePath = null
|
44
|
-
this.name = ep.name || ep.source
|
45
|
-
this.packageName = (/^adapt-/i.test(this.name) ? '' : 'adapt-') + (!isContrib ? '' : 'contrib-') + slug(this.name, { maintainCase: true })
|
46
|
-
// the constraint given by the user
|
47
|
-
this.requestedVersion = requestedVersion
|
48
|
-
// the most recent version of the plugin compatible with the given framework
|
49
|
-
this.latestCompatibleSourceVersion = null
|
50
|
-
// a non-wildcard constraint resolved to the highest version of the plugin that satisfies the requestedVersion and is compatible with the framework
|
51
|
-
this.matchedVersion = null
|
52
|
-
// a flag describing if the plugin can be updated
|
53
|
-
this.canBeUpdated = null
|
54
|
-
|
55
|
-
const isNameAPath = /\\|\//g.test(this.name)
|
56
|
-
const isVersionAPath = /\\|\//g.test(this.requestedVersion)
|
57
|
-
const isLocalPath = (isNameAPath || isVersionAPath)
|
58
|
-
if (isLocalPath) {
|
59
|
-
// wait to name the plugin until the local config file is loaded
|
60
|
-
this.sourcePath = isNameAPath ? this.name : this.requestedVersion
|
61
|
-
this.name = isVersionAPath ? this.packageName : ''
|
62
|
-
this.packageName = isNameAPath ? '' : this.packageName
|
63
|
-
this.requestedVersion = '*'
|
64
|
-
}
|
65
|
-
// the path of the source files
|
66
|
-
this.projectPath = null
|
67
|
-
// the project plugin .bower.json or bower.json
|
68
|
-
this._projectInfo = null
|
69
|
-
// the result of a query to the server or disk for source files
|
70
|
-
this._sourceInfo = null
|
71
|
-
// server given versions
|
72
|
-
this._versionsInfo = null
|
73
|
-
|
74
|
-
Plugin.instances.push(this)
|
75
|
-
}
|
76
|
-
|
77
|
-
/**
|
78
|
-
* the installed version is the latest version
|
79
|
-
* @returns {boolean|null}
|
80
|
-
*/
|
81
|
-
get isUpToDate () {
|
82
|
-
if (!this.hasFrameworkCompatibleVersion) return true;
|
83
|
-
const canCheckSourceAgainstProject = (this.latestSourceVersion && this.projectVersion)
|
84
|
-
if (!canCheckSourceAgainstProject) return null
|
85
|
-
const isLatestVersion = (this.projectVersion === this.latestSourceVersion)
|
86
|
-
const isLatestMatchedVersion = (this.projectVersion === this.matchedVersion)
|
87
|
-
const isProjectVersionGreater = semver.gt(this.projectVersion, this.matchedVersion)
|
88
|
-
return (isLatestVersion || isLatestMatchedVersion || isProjectVersionGreater)
|
89
|
-
}
|
90
|
-
|
91
|
-
/**
|
92
|
-
* the most recent version of the plugin
|
93
|
-
* @returns {string|null}
|
94
|
-
*/
|
95
|
-
get latestSourceVersion () {
|
96
|
-
return (this._sourceInfo?.version || null)
|
97
|
-
}
|
98
|
-
|
99
|
-
/**
|
100
|
-
* the installed version of the plugin
|
101
|
-
* @returns {string|null}
|
102
|
-
*/
|
103
|
-
get projectVersion () {
|
104
|
-
return (this._projectInfo?.version || null)
|
105
|
-
}
|
106
|
-
|
107
|
-
/**
|
108
|
-
* a list of tags denoting the source versions of the plugin
|
109
|
-
* @returns {[string]}
|
110
|
-
*/
|
111
|
-
get sourceVersions () {
|
112
|
-
return this._versionsInfo
|
113
|
-
}
|
114
|
-
|
115
|
-
/**
|
116
|
-
* plugin will be or was installed from a local source
|
117
|
-
* @returns {boolean}
|
118
|
-
*/
|
119
|
-
get isLocalSource () {
|
120
|
-
return Boolean(this.sourcePath || this?._projectInfo?._wasInstalledFromPath)
|
121
|
-
}
|
122
|
-
|
123
|
-
/**
|
124
|
-
* check if source path is a zip
|
125
|
-
* @returns {boolean}
|
126
|
-
*/
|
127
|
-
get isLocalSourceZip () {
|
128
|
-
return Boolean(this.isLocalSource && (this.sourcePath?.includes('.zip') || this._projectInfo?._source?.includes('.zip')))
|
129
|
-
}
|
130
|
-
|
131
|
-
/** @returns {boolean} */
|
132
|
-
get isVersioned () {
|
133
|
-
return Boolean(this.sourceVersions?.length)
|
134
|
-
}
|
135
|
-
|
136
|
-
/**
|
137
|
-
* is a contrib plugin
|
138
|
-
* @returns {boolean}
|
139
|
-
*/
|
140
|
-
get isContrib () {
|
141
|
-
return /^adapt-contrib/.test(this.packageName)
|
142
|
-
}
|
143
|
-
|
144
|
-
/**
|
145
|
-
* whether querying the server or disk for plugin information worked
|
146
|
-
* @returns {boolean}
|
147
|
-
*/
|
148
|
-
get isPresent () {
|
149
|
-
return Boolean(this._projectInfo || this._sourceInfo)
|
150
|
-
}
|
151
|
-
|
152
|
-
/**
|
153
|
-
* has user requested version
|
154
|
-
* @returns {boolean}
|
155
|
-
*/
|
156
|
-
get hasUserRequestVersion () {
|
157
|
-
return (this.requestedVersion !== '*')
|
158
|
-
}
|
159
|
-
|
160
|
-
/**
|
161
|
-
* the supplied a constraint is valid and supported by the plugin
|
162
|
-
* @returns {boolean|null}
|
163
|
-
*/
|
164
|
-
get hasValidRequestVersion () {
|
165
|
-
return (this.latestSourceVersion)
|
166
|
-
? semver.validRange(this.requestedVersion, semverOptions) &&
|
167
|
-
(this.isVersioned
|
168
|
-
? semver.maxSatisfying(this.sourceVersions, this.requestedVersion, semverOptions) !== null
|
169
|
-
: semver.satisfies(this.latestSourceVersion, this.requestedVersion)
|
170
|
-
)
|
171
|
-
: null
|
172
|
-
}
|
173
|
-
|
174
|
-
/** @returns {boolean} */
|
175
|
-
get hasFrameworkCompatibleVersion () {
|
176
|
-
return (this.latestCompatibleSourceVersion !== null)
|
177
|
-
}
|
178
|
-
|
179
|
-
async fetchSourceInfo () {
|
180
|
-
if (this.isLocalSource) return await this.fetchLocalSourceInfo()
|
181
|
-
await this.fetchBowerInfo()
|
182
|
-
}
|
183
|
-
|
184
|
-
async fetchLocalSourceInfo () {
|
185
|
-
if (this._sourceInfo) return this._sourceInfo
|
186
|
-
this._sourceInfo = null
|
187
|
-
if (!this.isLocalSource) throw new Error('Plugin name or version must be a path to the source')
|
188
|
-
if (this.isLocalSourceZip) throw new Error('Cannot install from zip files')
|
189
|
-
this._sourceInfo = await new Promise((resolve, reject) => {
|
190
|
-
// get bower.json data
|
191
|
-
const paths = [
|
192
|
-
path.resolve(this.cwd, `${this.sourcePath}/bower.json`)
|
193
|
-
]
|
194
|
-
const bowerJSON = paths.reduce((bowerJSON, bowerJSONPath) => {
|
195
|
-
if (bowerJSON) return bowerJSON
|
196
|
-
if (!fs.existsSync(bowerJSONPath)) return null
|
197
|
-
return fs.readJSONSync(bowerJSONPath)
|
198
|
-
}, null)
|
199
|
-
resolve(bowerJSON)
|
200
|
-
})
|
201
|
-
if (!this._sourceInfo) return
|
202
|
-
this.name = this._sourceInfo.name
|
203
|
-
this.matchedVersion = this.latestSourceVersion
|
204
|
-
this.packageName = this.name
|
205
|
-
}
|
206
|
-
|
207
|
-
async fetchBowerInfo () {
|
208
|
-
if (this._sourceInfo) return this._sourceInfo
|
209
|
-
this._sourceInfo = null
|
210
|
-
if (this.isLocalSource) return
|
211
|
-
const perform = async (attemptCount = 0) => {
|
212
|
-
try {
|
213
|
-
return await new Promise((resolve, reject) => {
|
214
|
-
bower.commands.info(`${this.packageName}`, null, { cwd: this.cwd, registry: this.BOWER_REGISTRY_CONFIG })
|
215
|
-
.on('end', resolve)
|
216
|
-
.on('error', reject)
|
217
|
-
})
|
218
|
-
} catch (err) {
|
219
|
-
const isFinished = (err?.code === 'ENOTFOUND' || attemptCount >= BOWER_MAX_TRY)
|
220
|
-
if (isFinished) return null
|
221
|
-
return await perform(attemptCount++)
|
222
|
-
}
|
223
|
-
}
|
224
|
-
const info = await perform()
|
225
|
-
if (!info) return
|
226
|
-
this._sourceInfo = info.latest
|
227
|
-
this._versionsInfo = info.versions.filter(version => semverOptions.includePrerelease ? true : !semver.prerelease(version))
|
228
|
-
}
|
229
|
-
|
230
|
-
async fetchProjectInfo () {
|
231
|
-
if (this._projectInfo) return this._projectInfo
|
232
|
-
this._projectInfo = null
|
233
|
-
this._projectInfo = await new Promise((resolve, reject) => {
|
234
|
-
// get bower.json data
|
235
|
-
globs([
|
236
|
-
`${this.cwd.replace(/\\/g, '/')}/src/*/${this.packageName}/.bower.json`,
|
237
|
-
`${this.cwd.replace(/\\/g, '/')}/src/*/${this.packageName}/bower.json`
|
238
|
-
], (err, matches) => {
|
239
|
-
if (err) return resolve(null)
|
240
|
-
const tester = new RegExp(`/${this.packageName}/`, 'i')
|
241
|
-
const match = matches.find(match => tester.test(match))
|
242
|
-
if (!match) {
|
243
|
-
// widen the search
|
244
|
-
globs([
|
245
|
-
`${this.cwd.replace(/\\/g, '/')}/src/**/.bower.json`,
|
246
|
-
`${this.cwd.replace(/\\/g, '/')}/src/**/bower.json`
|
247
|
-
], (err, matches) => {
|
248
|
-
if (err) return resolve(null)
|
249
|
-
const tester = new RegExp(`/${this.packageName}/`, 'i')
|
250
|
-
const match = matches.find(match => tester.test(match))
|
251
|
-
if (!match) return resolve(null)
|
252
|
-
this.projectPath = path.resolve(match, '../')
|
253
|
-
resolve(fs.readJSONSync(match))
|
254
|
-
})
|
255
|
-
return
|
256
|
-
}
|
257
|
-
this.projectPath = path.resolve(match, '../')
|
258
|
-
resolve(fs.readJSONSync(match))
|
259
|
-
})
|
260
|
-
})
|
261
|
-
if (!this._projectInfo) return
|
262
|
-
this.name = this._projectInfo.name
|
263
|
-
this.packageName = this.name
|
264
|
-
}
|
265
|
-
|
266
|
-
async findCompatibleVersion (framework) {
|
267
|
-
const getBowerVersionInfo = async (version) => {
|
268
|
-
const perform = async (attemptCount = 0) => {
|
269
|
-
try {
|
270
|
-
return await new Promise((resolve, reject) => {
|
271
|
-
bower.commands.info(`${this.packageName}@${version}`, null, { cwd: this.cwd, registry: this.BOWER_REGISTRY_CONFIG })
|
272
|
-
.on('end', resolve)
|
273
|
-
.on('error', reject)
|
274
|
-
})
|
275
|
-
} catch (err) {
|
276
|
-
const isFinished = (err?.code === 'ENOTFOUND' || attemptCount >= BOWER_MAX_TRY)
|
277
|
-
if (isFinished) return null
|
278
|
-
return await perform(attemptCount++)
|
279
|
-
}
|
280
|
-
}
|
281
|
-
return await perform()
|
282
|
-
}
|
283
|
-
const getMatchingVersion = async () => {
|
284
|
-
if (this.isLocalSource) {
|
285
|
-
const info = this.projectVersion ? this._projectInfo : this._sourceInfo
|
286
|
-
const satisfiesConstraint = !this.hasValidRequestVersion || semver.satisfies(info.version, this.requestedVersion, semverOptions)
|
287
|
-
const satisfiesFramework = semver.satisfies(framework, info.framework)
|
288
|
-
if (satisfiesFramework && satisfiesConstraint) this.latestCompatibleSourceVersion = info.version
|
289
|
-
return info.version
|
290
|
-
}
|
291
|
-
|
292
|
-
if (!this.isPresent) return null
|
293
|
-
|
294
|
-
// check if the latest version is compatible
|
295
|
-
const satisfiesConstraint = !this.hasValidRequestVersion || semver.satisfies(this._sourceInfo.version, this.requestedVersion, semverOptions)
|
296
|
-
const satisfiesFramework = semver.satisfies(framework, this._sourceInfo.framework, semverOptions)
|
297
|
-
if (!this.latestCompatibleSourceVersion && satisfiesFramework) this.latestCompatibleSourceVersion = this.latestSourceVersion
|
298
|
-
if (satisfiesConstraint && satisfiesFramework) {
|
299
|
-
return this.latestSourceVersion
|
300
|
-
}
|
301
|
-
|
302
|
-
if (!this.isVersioned) return null
|
303
|
-
|
304
|
-
// find the highest version that is compatible with the framework and constraint
|
305
|
-
const searchVersionInfo = async (framework, versionIndex = 0) => {
|
306
|
-
const versioninfo = await getBowerVersionInfo(this.sourceVersions[versionIndex])
|
307
|
-
// give up if there is any failure to obtain version info
|
308
|
-
if (!this.isPresent) return null
|
309
|
-
// check that the proposed plugin is compatible with the contraint and installed framework
|
310
|
-
const satisfiesConstraint = !this.hasValidRequestVersion || semver.satisfies(versioninfo.version, this.requestedVersion, semverOptions)
|
311
|
-
const satisfiesFramework = semver.satisfies(framework, versioninfo.framework, semverOptions)
|
312
|
-
if (!this.latestCompatibleSourceVersion && satisfiesFramework) this.latestCompatibleSourceVersion = versioninfo.version
|
313
|
-
const checkNext = (!satisfiesFramework || !satisfiesConstraint)
|
314
|
-
const hasNoMoreVersions = (versionIndex + 1 >= this.sourceVersions.length)
|
315
|
-
if (checkNext && hasNoMoreVersions) return null
|
316
|
-
if (checkNext) return await searchVersionInfo(framework, versionIndex + 1)
|
317
|
-
return versioninfo.version
|
318
|
-
}
|
319
|
-
return await searchVersionInfo(framework)
|
320
|
-
}
|
321
|
-
this.matchedVersion = await getMatchingVersion()
|
322
|
-
this.canBeUpdated = (this.projectVersion && this.matchedVersion) && (this.projectVersion !== this.matchedVersion)
|
323
|
-
}
|
324
|
-
|
325
|
-
/**
|
326
|
-
* @returns {string}
|
327
|
-
*/
|
328
|
-
async getType () {
|
329
|
-
if (this._type) return this._type
|
330
|
-
const info = await this.getInfo()
|
331
|
-
const foundAttributeType = PLUGIN_TYPES.find(type => info[type])
|
332
|
-
const foundKeywordType = info.keywords
|
333
|
-
.map(keyword => {
|
334
|
-
const typematches = PLUGIN_TYPES.filter(type => keyword?.toLowerCase()?.includes(type))
|
335
|
-
return typematches.length ? typematches[0] : null
|
336
|
-
})
|
337
|
-
.filter(Boolean)[0]
|
338
|
-
return (this._type = foundAttributeType || foundKeywordType || PLUGIN_DEFAULT_TYPE)
|
339
|
-
}
|
340
|
-
|
341
|
-
async getTypeFolder () {
|
342
|
-
const type = await this.getType()
|
343
|
-
return PLUGIN_TYPE_FOLDERS[type]
|
344
|
-
}
|
345
|
-
|
346
|
-
async getInfo () {
|
347
|
-
if (this._projectInfo) return this._projectInfo
|
348
|
-
if (!this._sourceInfo) await this.fetchSourceInfo()
|
349
|
-
return this._sourceInfo
|
350
|
-
}
|
351
|
-
|
352
|
-
async getRepositoryUrl () {
|
353
|
-
if (this._repositoryUrl) return this._repositoryUrl
|
354
|
-
if (this.isLocalSource) return
|
355
|
-
const url = await new Promise((resolve, reject) => {
|
356
|
-
bower.commands.lookup(this.packageName, { cwd: this.cwd, registry: this.BOWER_REGISTRY_CONFIG })
|
357
|
-
.on('end', resolve)
|
358
|
-
.on('error', reject)
|
359
|
-
})
|
360
|
-
return (this._repositoryUrl = url)
|
361
|
-
}
|
362
|
-
|
363
|
-
/** @returns {string} */
|
364
|
-
toString () {
|
365
|
-
const isAny = (this.projectVersion === '*' || this.projectVersion === null)
|
366
|
-
return `${this.packageName}${isAny ? '' : `@${this.projectVersion}`}`
|
367
|
-
}
|
368
|
-
|
369
|
-
async getSchemaPaths () {
|
370
|
-
if (this.isLocalSource) await this.fetchLocalSourceInfo()
|
371
|
-
else if (this.project) await this.fetchProjectInfo()
|
372
|
-
else throw new Error(`Cannot fetch schemas from remote plugin: ${this.name}`)
|
373
|
-
const pluginPath = this.projectPath ?? this.sourcePath
|
374
|
-
return new Promise((resolve, reject) => {
|
375
|
-
return globs(path.resolve(this.cwd, pluginPath, '**/*.schema.json'), (err, matches) => {
|
376
|
-
if (err) return reject(err)
|
377
|
-
resolve(matches)
|
378
|
-
})
|
379
|
-
})
|
380
|
-
}
|
381
|
-
|
382
|
-
/**
|
383
|
-
* Read plugin data from pluginPath
|
384
|
-
* @param {Object} options
|
385
|
-
* @param {string} options.pluginPath Path to source directory
|
386
|
-
* @param {string} [options.projectPath=process.cwd()] Optional path to potential installation project
|
387
|
-
* @returns {Plugin}
|
388
|
-
*/
|
389
|
-
static async fromPath ({
|
390
|
-
pluginPath,
|
391
|
-
projectPath = process.cwd()
|
392
|
-
}) {
|
393
|
-
const plugin = new Plugin({
|
394
|
-
name: pluginPath,
|
395
|
-
cwd: projectPath
|
396
|
-
})
|
397
|
-
await plugin.fetchLocalSourceInfo()
|
398
|
-
return plugin
|
399
|
-
}
|
400
|
-
|
401
|
-
static get instances () {
|
402
|
-
return (Plugin._instances = Plugin._instances || [])
|
403
|
-
}
|
404
|
-
}
|
1
|
+
import slug from 'speakingurl'
|
2
|
+
import globs from 'globs'
|
3
|
+
import bower from 'bower'
|
4
|
+
import endpointParser from 'bower-endpoint-parser'
|
5
|
+
import semver from 'semver'
|
6
|
+
import fs from 'fs-extra'
|
7
|
+
import path from 'path'
|
8
|
+
import getBowerRegistryConfig from './getBowerRegistryConfig.js'
|
9
|
+
import { ADAPT_ALLOW_PRERELEASE, PLUGIN_TYPES, PLUGIN_TYPE_FOLDERS, PLUGIN_DEFAULT_TYPE } from '../util/constants.js'
|
10
|
+
/** @typedef {import("./Project.js").default} Project */
|
11
|
+
const semverOptions = { includePrerelease: ADAPT_ALLOW_PRERELEASE }
|
12
|
+
|
13
|
+
// when a bower command errors this is the maximum number of attempts the command will be repeated
|
14
|
+
const BOWER_MAX_TRY = 5
|
15
|
+
|
16
|
+
export default class Plugin {
|
17
|
+
/**
|
18
|
+
* @param {Object} options
|
19
|
+
* @param {string} options.name
|
20
|
+
* @param {string} options.requestedVersion
|
21
|
+
* @param {boolean} options.isContrib
|
22
|
+
* @param {boolean} options.isCompatibleEnabled whether to target the latest compatible version for all plugin installations (overrides requestedVersion)
|
23
|
+
* @param {Project} options.project
|
24
|
+
* @param {string} options.cwd
|
25
|
+
* @param {Object} options.logger
|
26
|
+
*/
|
27
|
+
constructor ({
|
28
|
+
name,
|
29
|
+
requestedVersion = '*',
|
30
|
+
isContrib = false,
|
31
|
+
isCompatibleEnabled = false,
|
32
|
+
project,
|
33
|
+
cwd = (project?.cwd ?? process.cwd()),
|
34
|
+
logger
|
35
|
+
} = {}) {
|
36
|
+
this.logger = logger
|
37
|
+
/** @type {Project} */
|
38
|
+
this.project = project
|
39
|
+
this.cwd = cwd
|
40
|
+
this.BOWER_REGISTRY_CONFIG = getBowerRegistryConfig({ cwd: this.cwd })
|
41
|
+
const endpoint = name + '#' + (isCompatibleEnabled ? '*' : requestedVersion)
|
42
|
+
const ep = endpointParser.decompose(endpoint)
|
43
|
+
this.sourcePath = null
|
44
|
+
this.name = ep.name || ep.source
|
45
|
+
this.packageName = (/^adapt-/i.test(this.name) ? '' : 'adapt-') + (!isContrib ? '' : 'contrib-') + slug(this.name, { maintainCase: true })
|
46
|
+
// the constraint given by the user
|
47
|
+
this.requestedVersion = requestedVersion
|
48
|
+
// the most recent version of the plugin compatible with the given framework
|
49
|
+
this.latestCompatibleSourceVersion = null
|
50
|
+
// a non-wildcard constraint resolved to the highest version of the plugin that satisfies the requestedVersion and is compatible with the framework
|
51
|
+
this.matchedVersion = null
|
52
|
+
// a flag describing if the plugin can be updated
|
53
|
+
this.canBeUpdated = null
|
54
|
+
|
55
|
+
const isNameAPath = /\\|\//g.test(this.name)
|
56
|
+
const isVersionAPath = /\\|\//g.test(this.requestedVersion)
|
57
|
+
const isLocalPath = (isNameAPath || isVersionAPath)
|
58
|
+
if (isLocalPath) {
|
59
|
+
// wait to name the plugin until the local config file is loaded
|
60
|
+
this.sourcePath = isNameAPath ? this.name : this.requestedVersion
|
61
|
+
this.name = isVersionAPath ? this.packageName : ''
|
62
|
+
this.packageName = isNameAPath ? '' : this.packageName
|
63
|
+
this.requestedVersion = '*'
|
64
|
+
}
|
65
|
+
// the path of the source files
|
66
|
+
this.projectPath = null
|
67
|
+
// the project plugin .bower.json or bower.json
|
68
|
+
this._projectInfo = null
|
69
|
+
// the result of a query to the server or disk for source files
|
70
|
+
this._sourceInfo = null
|
71
|
+
// server given versions
|
72
|
+
this._versionsInfo = null
|
73
|
+
|
74
|
+
Plugin.instances.push(this)
|
75
|
+
}
|
76
|
+
|
77
|
+
/**
|
78
|
+
* the installed version is the latest version
|
79
|
+
* @returns {boolean|null}
|
80
|
+
*/
|
81
|
+
get isUpToDate () {
|
82
|
+
if (!this.hasFrameworkCompatibleVersion) return true;
|
83
|
+
const canCheckSourceAgainstProject = (this.latestSourceVersion && this.projectVersion)
|
84
|
+
if (!canCheckSourceAgainstProject) return null
|
85
|
+
const isLatestVersion = (this.projectVersion === this.latestSourceVersion)
|
86
|
+
const isLatestMatchedVersion = (this.projectVersion === this.matchedVersion)
|
87
|
+
const isProjectVersionGreater = semver.gt(this.projectVersion, this.matchedVersion)
|
88
|
+
return (isLatestVersion || isLatestMatchedVersion || isProjectVersionGreater)
|
89
|
+
}
|
90
|
+
|
91
|
+
/**
|
92
|
+
* the most recent version of the plugin
|
93
|
+
* @returns {string|null}
|
94
|
+
*/
|
95
|
+
get latestSourceVersion () {
|
96
|
+
return (this._sourceInfo?.version || null)
|
97
|
+
}
|
98
|
+
|
99
|
+
/**
|
100
|
+
* the installed version of the plugin
|
101
|
+
* @returns {string|null}
|
102
|
+
*/
|
103
|
+
get projectVersion () {
|
104
|
+
return (this._projectInfo?.version || null)
|
105
|
+
}
|
106
|
+
|
107
|
+
/**
|
108
|
+
* a list of tags denoting the source versions of the plugin
|
109
|
+
* @returns {[string]}
|
110
|
+
*/
|
111
|
+
get sourceVersions () {
|
112
|
+
return this._versionsInfo
|
113
|
+
}
|
114
|
+
|
115
|
+
/**
|
116
|
+
* plugin will be or was installed from a local source
|
117
|
+
* @returns {boolean}
|
118
|
+
*/
|
119
|
+
get isLocalSource () {
|
120
|
+
return Boolean(this.sourcePath || this?._projectInfo?._wasInstalledFromPath)
|
121
|
+
}
|
122
|
+
|
123
|
+
/**
|
124
|
+
* check if source path is a zip
|
125
|
+
* @returns {boolean}
|
126
|
+
*/
|
127
|
+
get isLocalSourceZip () {
|
128
|
+
return Boolean(this.isLocalSource && (this.sourcePath?.includes('.zip') || this._projectInfo?._source?.includes('.zip')))
|
129
|
+
}
|
130
|
+
|
131
|
+
/** @returns {boolean} */
|
132
|
+
get isVersioned () {
|
133
|
+
return Boolean(this.sourceVersions?.length)
|
134
|
+
}
|
135
|
+
|
136
|
+
/**
|
137
|
+
* is a contrib plugin
|
138
|
+
* @returns {boolean}
|
139
|
+
*/
|
140
|
+
get isContrib () {
|
141
|
+
return /^adapt-contrib/.test(this.packageName)
|
142
|
+
}
|
143
|
+
|
144
|
+
/**
|
145
|
+
* whether querying the server or disk for plugin information worked
|
146
|
+
* @returns {boolean}
|
147
|
+
*/
|
148
|
+
get isPresent () {
|
149
|
+
return Boolean(this._projectInfo || this._sourceInfo)
|
150
|
+
}
|
151
|
+
|
152
|
+
/**
|
153
|
+
* has user requested version
|
154
|
+
* @returns {boolean}
|
155
|
+
*/
|
156
|
+
get hasUserRequestVersion () {
|
157
|
+
return (this.requestedVersion !== '*')
|
158
|
+
}
|
159
|
+
|
160
|
+
/**
|
161
|
+
* the supplied a constraint is valid and supported by the plugin
|
162
|
+
* @returns {boolean|null}
|
163
|
+
*/
|
164
|
+
get hasValidRequestVersion () {
|
165
|
+
return (this.latestSourceVersion)
|
166
|
+
? semver.validRange(this.requestedVersion, semverOptions) &&
|
167
|
+
(this.isVersioned
|
168
|
+
? semver.maxSatisfying(this.sourceVersions, this.requestedVersion, semverOptions) !== null
|
169
|
+
: semver.satisfies(this.latestSourceVersion, this.requestedVersion)
|
170
|
+
)
|
171
|
+
: null
|
172
|
+
}
|
173
|
+
|
174
|
+
/** @returns {boolean} */
|
175
|
+
get hasFrameworkCompatibleVersion () {
|
176
|
+
return (this.latestCompatibleSourceVersion !== null)
|
177
|
+
}
|
178
|
+
|
179
|
+
async fetchSourceInfo () {
|
180
|
+
if (this.isLocalSource) return await this.fetchLocalSourceInfo()
|
181
|
+
await this.fetchBowerInfo()
|
182
|
+
}
|
183
|
+
|
184
|
+
async fetchLocalSourceInfo () {
|
185
|
+
if (this._sourceInfo) return this._sourceInfo
|
186
|
+
this._sourceInfo = null
|
187
|
+
if (!this.isLocalSource) throw new Error('Plugin name or version must be a path to the source')
|
188
|
+
if (this.isLocalSourceZip) throw new Error('Cannot install from zip files')
|
189
|
+
this._sourceInfo = await new Promise((resolve, reject) => {
|
190
|
+
// get bower.json data
|
191
|
+
const paths = [
|
192
|
+
path.resolve(this.cwd, `${this.sourcePath}/bower.json`)
|
193
|
+
]
|
194
|
+
const bowerJSON = paths.reduce((bowerJSON, bowerJSONPath) => {
|
195
|
+
if (bowerJSON) return bowerJSON
|
196
|
+
if (!fs.existsSync(bowerJSONPath)) return null
|
197
|
+
return fs.readJSONSync(bowerJSONPath)
|
198
|
+
}, null)
|
199
|
+
resolve(bowerJSON)
|
200
|
+
})
|
201
|
+
if (!this._sourceInfo) return
|
202
|
+
this.name = this._sourceInfo.name
|
203
|
+
this.matchedVersion = this.latestSourceVersion
|
204
|
+
this.packageName = this.name
|
205
|
+
}
|
206
|
+
|
207
|
+
async fetchBowerInfo () {
|
208
|
+
if (this._sourceInfo) return this._sourceInfo
|
209
|
+
this._sourceInfo = null
|
210
|
+
if (this.isLocalSource) return
|
211
|
+
const perform = async (attemptCount = 0) => {
|
212
|
+
try {
|
213
|
+
return await new Promise((resolve, reject) => {
|
214
|
+
bower.commands.info(`${this.packageName}`, null, { cwd: this.cwd, registry: this.BOWER_REGISTRY_CONFIG })
|
215
|
+
.on('end', resolve)
|
216
|
+
.on('error', reject)
|
217
|
+
})
|
218
|
+
} catch (err) {
|
219
|
+
const isFinished = (err?.code === 'ENOTFOUND' || attemptCount >= BOWER_MAX_TRY)
|
220
|
+
if (isFinished) return null
|
221
|
+
return await perform(attemptCount++)
|
222
|
+
}
|
223
|
+
}
|
224
|
+
const info = await perform()
|
225
|
+
if (!info) return
|
226
|
+
this._sourceInfo = info.latest
|
227
|
+
this._versionsInfo = info.versions.filter(version => semverOptions.includePrerelease ? true : !semver.prerelease(version))
|
228
|
+
}
|
229
|
+
|
230
|
+
async fetchProjectInfo () {
|
231
|
+
if (this._projectInfo) return this._projectInfo
|
232
|
+
this._projectInfo = null
|
233
|
+
this._projectInfo = await new Promise((resolve, reject) => {
|
234
|
+
// get bower.json data
|
235
|
+
globs([
|
236
|
+
`${this.cwd.replace(/\\/g, '/')}/src/*/${this.packageName}/.bower.json`,
|
237
|
+
`${this.cwd.replace(/\\/g, '/')}/src/*/${this.packageName}/bower.json`
|
238
|
+
], (err, matches) => {
|
239
|
+
if (err) return resolve(null)
|
240
|
+
const tester = new RegExp(`/${this.packageName}/`, 'i')
|
241
|
+
const match = matches.find(match => tester.test(match))
|
242
|
+
if (!match) {
|
243
|
+
// widen the search
|
244
|
+
globs([
|
245
|
+
`${this.cwd.replace(/\\/g, '/')}/src/**/.bower.json`,
|
246
|
+
`${this.cwd.replace(/\\/g, '/')}/src/**/bower.json`
|
247
|
+
], (err, matches) => {
|
248
|
+
if (err) return resolve(null)
|
249
|
+
const tester = new RegExp(`/${this.packageName}/`, 'i')
|
250
|
+
const match = matches.find(match => tester.test(match))
|
251
|
+
if (!match) return resolve(null)
|
252
|
+
this.projectPath = path.resolve(match, '../')
|
253
|
+
resolve(fs.readJSONSync(match))
|
254
|
+
})
|
255
|
+
return
|
256
|
+
}
|
257
|
+
this.projectPath = path.resolve(match, '../')
|
258
|
+
resolve(fs.readJSONSync(match))
|
259
|
+
})
|
260
|
+
})
|
261
|
+
if (!this._projectInfo) return
|
262
|
+
this.name = this._projectInfo.name
|
263
|
+
this.packageName = this.name
|
264
|
+
}
|
265
|
+
|
266
|
+
async findCompatibleVersion (framework) {
|
267
|
+
const getBowerVersionInfo = async (version) => {
|
268
|
+
const perform = async (attemptCount = 0) => {
|
269
|
+
try {
|
270
|
+
return await new Promise((resolve, reject) => {
|
271
|
+
bower.commands.info(`${this.packageName}@${version}`, null, { cwd: this.cwd, registry: this.BOWER_REGISTRY_CONFIG })
|
272
|
+
.on('end', resolve)
|
273
|
+
.on('error', reject)
|
274
|
+
})
|
275
|
+
} catch (err) {
|
276
|
+
const isFinished = (err?.code === 'ENOTFOUND' || attemptCount >= BOWER_MAX_TRY)
|
277
|
+
if (isFinished) return null
|
278
|
+
return await perform(attemptCount++)
|
279
|
+
}
|
280
|
+
}
|
281
|
+
return await perform()
|
282
|
+
}
|
283
|
+
const getMatchingVersion = async () => {
|
284
|
+
if (this.isLocalSource) {
|
285
|
+
const info = this.projectVersion ? this._projectInfo : this._sourceInfo
|
286
|
+
const satisfiesConstraint = !this.hasValidRequestVersion || semver.satisfies(info.version, this.requestedVersion, semverOptions)
|
287
|
+
const satisfiesFramework = semver.satisfies(framework, info.framework)
|
288
|
+
if (satisfiesFramework && satisfiesConstraint) this.latestCompatibleSourceVersion = info.version
|
289
|
+
return info.version
|
290
|
+
}
|
291
|
+
|
292
|
+
if (!this.isPresent) return null
|
293
|
+
|
294
|
+
// check if the latest version is compatible
|
295
|
+
const satisfiesConstraint = !this.hasValidRequestVersion || semver.satisfies(this._sourceInfo.version, this.requestedVersion, semverOptions)
|
296
|
+
const satisfiesFramework = semver.satisfies(framework, this._sourceInfo.framework, semverOptions)
|
297
|
+
if (!this.latestCompatibleSourceVersion && satisfiesFramework) this.latestCompatibleSourceVersion = this.latestSourceVersion
|
298
|
+
if (satisfiesConstraint && satisfiesFramework) {
|
299
|
+
return this.latestSourceVersion
|
300
|
+
}
|
301
|
+
|
302
|
+
if (!this.isVersioned) return null
|
303
|
+
|
304
|
+
// find the highest version that is compatible with the framework and constraint
|
305
|
+
const searchVersionInfo = async (framework, versionIndex = 0) => {
|
306
|
+
const versioninfo = await getBowerVersionInfo(this.sourceVersions[versionIndex])
|
307
|
+
// give up if there is any failure to obtain version info
|
308
|
+
if (!this.isPresent) return null
|
309
|
+
// check that the proposed plugin is compatible with the contraint and installed framework
|
310
|
+
const satisfiesConstraint = !this.hasValidRequestVersion || semver.satisfies(versioninfo.version, this.requestedVersion, semverOptions)
|
311
|
+
const satisfiesFramework = semver.satisfies(framework, versioninfo.framework, semverOptions)
|
312
|
+
if (!this.latestCompatibleSourceVersion && satisfiesFramework) this.latestCompatibleSourceVersion = versioninfo.version
|
313
|
+
const checkNext = (!satisfiesFramework || !satisfiesConstraint)
|
314
|
+
const hasNoMoreVersions = (versionIndex + 1 >= this.sourceVersions.length)
|
315
|
+
if (checkNext && hasNoMoreVersions) return null
|
316
|
+
if (checkNext) return await searchVersionInfo(framework, versionIndex + 1)
|
317
|
+
return versioninfo.version
|
318
|
+
}
|
319
|
+
return await searchVersionInfo(framework)
|
320
|
+
}
|
321
|
+
this.matchedVersion = await getMatchingVersion()
|
322
|
+
this.canBeUpdated = (this.projectVersion && this.matchedVersion) && (this.projectVersion !== this.matchedVersion)
|
323
|
+
}
|
324
|
+
|
325
|
+
/**
|
326
|
+
* @returns {string}
|
327
|
+
*/
|
328
|
+
async getType () {
|
329
|
+
if (this._type) return this._type
|
330
|
+
const info = await this.getInfo()
|
331
|
+
const foundAttributeType = PLUGIN_TYPES.find(type => info[type])
|
332
|
+
const foundKeywordType = info.keywords
|
333
|
+
.map(keyword => {
|
334
|
+
const typematches = PLUGIN_TYPES.filter(type => keyword?.toLowerCase()?.includes(type))
|
335
|
+
return typematches.length ? typematches[0] : null
|
336
|
+
})
|
337
|
+
.filter(Boolean)[0]
|
338
|
+
return (this._type = foundAttributeType || foundKeywordType || PLUGIN_DEFAULT_TYPE)
|
339
|
+
}
|
340
|
+
|
341
|
+
async getTypeFolder () {
|
342
|
+
const type = await this.getType()
|
343
|
+
return PLUGIN_TYPE_FOLDERS[type]
|
344
|
+
}
|
345
|
+
|
346
|
+
async getInfo () {
|
347
|
+
if (this._projectInfo) return this._projectInfo
|
348
|
+
if (!this._sourceInfo) await this.fetchSourceInfo()
|
349
|
+
return this._sourceInfo
|
350
|
+
}
|
351
|
+
|
352
|
+
async getRepositoryUrl () {
|
353
|
+
if (this._repositoryUrl) return this._repositoryUrl
|
354
|
+
if (this.isLocalSource) return
|
355
|
+
const url = await new Promise((resolve, reject) => {
|
356
|
+
bower.commands.lookup(this.packageName, { cwd: this.cwd, registry: this.BOWER_REGISTRY_CONFIG })
|
357
|
+
.on('end', resolve)
|
358
|
+
.on('error', reject)
|
359
|
+
})
|
360
|
+
return (this._repositoryUrl = url)
|
361
|
+
}
|
362
|
+
|
363
|
+
/** @returns {string} */
|
364
|
+
toString () {
|
365
|
+
const isAny = (this.projectVersion === '*' || this.projectVersion === null)
|
366
|
+
return `${this.packageName}${isAny ? '' : `@${this.projectVersion}`}`
|
367
|
+
}
|
368
|
+
|
369
|
+
async getSchemaPaths () {
|
370
|
+
if (this.isLocalSource) await this.fetchLocalSourceInfo()
|
371
|
+
else if (this.project) await this.fetchProjectInfo()
|
372
|
+
else throw new Error(`Cannot fetch schemas from remote plugin: ${this.name}`)
|
373
|
+
const pluginPath = this.projectPath ?? this.sourcePath
|
374
|
+
return new Promise((resolve, reject) => {
|
375
|
+
return globs(path.resolve(this.cwd, pluginPath, '**/*.schema.json'), (err, matches) => {
|
376
|
+
if (err) return reject(err)
|
377
|
+
resolve(matches)
|
378
|
+
})
|
379
|
+
})
|
380
|
+
}
|
381
|
+
|
382
|
+
/**
|
383
|
+
* Read plugin data from pluginPath
|
384
|
+
* @param {Object} options
|
385
|
+
* @param {string} options.pluginPath Path to source directory
|
386
|
+
* @param {string} [options.projectPath=process.cwd()] Optional path to potential installation project
|
387
|
+
* @returns {Plugin}
|
388
|
+
*/
|
389
|
+
static async fromPath ({
|
390
|
+
pluginPath,
|
391
|
+
projectPath = process.cwd()
|
392
|
+
}) {
|
393
|
+
const plugin = new Plugin({
|
394
|
+
name: pluginPath,
|
395
|
+
cwd: projectPath
|
396
|
+
})
|
397
|
+
await plugin.fetchLocalSourceInfo()
|
398
|
+
return plugin
|
399
|
+
}
|
400
|
+
|
401
|
+
static get instances () {
|
402
|
+
return (Plugin._instances = Plugin._instances || [])
|
403
|
+
}
|
404
|
+
}
|