adapt-project 1.0.0
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/.github/workflows/releases.yml +32 -0
- package/.github/workflows/standardjs.yml +13 -0
- package/.github/workflows/tests.yml +13 -0
- package/README.md +168 -0
- package/eslint.config.js +10 -0
- package/index.js +13 -0
- package/lib/Data.js +185 -0
- package/lib/Framework.js +295 -0
- package/lib/JSONFile.js +104 -0
- package/lib/JSONFileItem.js +27 -0
- package/lib/Plugins.js +82 -0
- package/lib/Schemas.js +208 -0
- package/lib/Translate.js +495 -0
- package/lib/data/Language.js +301 -0
- package/lib/data/LanguageFile.js +35 -0
- package/lib/plugins/Plugin.js +143 -0
- package/lib/schema/ExtensionSchema.js +26 -0
- package/lib/schema/GlobalsSchema.js +65 -0
- package/lib/schema/ModelSchema.js +45 -0
- package/lib/schema/ModelSchemas.js +41 -0
- package/lib/schema/Schema.js +191 -0
- package/package.json +54 -0
package/lib/Framework.js
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import JSONFile from './JSONFile.js'
|
|
3
|
+
import Data from './Data.js'
|
|
4
|
+
import Translate from './Translate.js'
|
|
5
|
+
import Plugins from './Plugins.js'
|
|
6
|
+
import Schemas from './Schemas.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {import('./JSONFileItem')} JSONFileItem
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The class represents an Adapt Framework root directory. It provides APIs for
|
|
14
|
+
* plugins, schemas, data and translations.
|
|
15
|
+
*/
|
|
16
|
+
class Framework {
|
|
17
|
+
/**
|
|
18
|
+
* @param {Object} options
|
|
19
|
+
* @param {string} options.rootPath
|
|
20
|
+
* @param {string} options.outputPath
|
|
21
|
+
* @param {string} options.sourcePath
|
|
22
|
+
* @param {function} options.includedFilter
|
|
23
|
+
* @param {string} options.jsonext
|
|
24
|
+
* @param {string} options.trackingIdType,
|
|
25
|
+
* @param {boolean} options.useOutputData
|
|
26
|
+
* @param {function} options.log
|
|
27
|
+
* @param {function} options.warn
|
|
28
|
+
*/
|
|
29
|
+
constructor ({
|
|
30
|
+
rootPath = process.cwd(),
|
|
31
|
+
outputPath = path.join(rootPath, '/build/'),
|
|
32
|
+
sourcePath = path.join(rootPath, '/src/'),
|
|
33
|
+
courseDir = 'course',
|
|
34
|
+
includedFilter = function () { return true },
|
|
35
|
+
jsonext = 'json',
|
|
36
|
+
trackingIdType = 'block',
|
|
37
|
+
useOutputData = false,
|
|
38
|
+
log = console.log,
|
|
39
|
+
warn = console.warn
|
|
40
|
+
} = {}) {
|
|
41
|
+
/** @type {string} */
|
|
42
|
+
this.rootPath = rootPath.replace(/\\/g, '/')
|
|
43
|
+
/** @type {string} */
|
|
44
|
+
this.outputPath = path.resolve(this.rootPath, outputPath).replace(/\\/g, '/').replace(/\/?$/, '/')
|
|
45
|
+
/** @type {string} */
|
|
46
|
+
this.sourcePath = path.resolve(this.rootPath, sourcePath).replace(/\\/g, '/').replace(/\/?$/, '/')
|
|
47
|
+
/** @type {string} */
|
|
48
|
+
this.courseDir = courseDir
|
|
49
|
+
/** @type {function} */
|
|
50
|
+
this.includedFilter = includedFilter
|
|
51
|
+
/** @type {string} */
|
|
52
|
+
this.jsonext = jsonext
|
|
53
|
+
/** @type {string} */
|
|
54
|
+
this.trackingIdType = trackingIdType
|
|
55
|
+
/** @type {boolean} */
|
|
56
|
+
this.useOutputData = useOutputData
|
|
57
|
+
/** @type {function} */
|
|
58
|
+
this.log = log
|
|
59
|
+
/** @type {function} */
|
|
60
|
+
this.warn = warn
|
|
61
|
+
/** @type {JSONFile} */
|
|
62
|
+
this.packageJSONFile = null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** @returns {Framework} */
|
|
66
|
+
load () {
|
|
67
|
+
this.packageJSONFile = new JSONFile({
|
|
68
|
+
framework: this,
|
|
69
|
+
path: path.join(this.rootPath, 'package.json').replace(/\\/g, '/')
|
|
70
|
+
})
|
|
71
|
+
this.packageJSONFile.load()
|
|
72
|
+
return this
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** @returns {JSONFileItem} */
|
|
76
|
+
getPackageJSONFileItem () {
|
|
77
|
+
return this.packageJSONFile.firstFileItem
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** @returns {string} */
|
|
81
|
+
get version () {
|
|
82
|
+
return this.getPackageJSONFileItem().item.version
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Returns a Data instance for either the src/course or build/course folder
|
|
87
|
+
* depending on the specification of the useOutputData property on either the
|
|
88
|
+
* function or the Framework instance.
|
|
89
|
+
* @returns {Data}
|
|
90
|
+
*/
|
|
91
|
+
getData ({
|
|
92
|
+
useOutputData = this.useOutputData,
|
|
93
|
+
performLoad = true
|
|
94
|
+
} = {}) {
|
|
95
|
+
const data = new Data({
|
|
96
|
+
framework: this,
|
|
97
|
+
sourcePath: useOutputData ? this.outputPath : this.sourcePath,
|
|
98
|
+
courseDir: this.courseDir,
|
|
99
|
+
jsonext: this.jsonext,
|
|
100
|
+
trackingIdType: this.trackingIdType,
|
|
101
|
+
log: this.log
|
|
102
|
+
})
|
|
103
|
+
if (performLoad) data.load()
|
|
104
|
+
return data
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** @returns {Plugins} */
|
|
108
|
+
getPlugins ({
|
|
109
|
+
includedFilter = this.includedFilter
|
|
110
|
+
} = {}) {
|
|
111
|
+
const plugins = new Plugins({
|
|
112
|
+
framework: this.framework,
|
|
113
|
+
includedFilter,
|
|
114
|
+
sourcePath: this.sourcePath,
|
|
115
|
+
log: this.log,
|
|
116
|
+
warn: this.warn
|
|
117
|
+
})
|
|
118
|
+
plugins.load()
|
|
119
|
+
return plugins
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** @returns {Schemas} */
|
|
123
|
+
getSchemas ({
|
|
124
|
+
includedFilter = this.includedFilter
|
|
125
|
+
} = {}) {
|
|
126
|
+
const schemas = new Schemas({
|
|
127
|
+
framework: this,
|
|
128
|
+
includedFilter,
|
|
129
|
+
sourcePath: this.sourcePath,
|
|
130
|
+
log: this.log
|
|
131
|
+
})
|
|
132
|
+
schemas.load()
|
|
133
|
+
return schemas
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** @returns {Translate} */
|
|
137
|
+
getTranslate ({
|
|
138
|
+
includedFilter = this.includedFilter,
|
|
139
|
+
masterLang = 'en',
|
|
140
|
+
targetLang = null,
|
|
141
|
+
format = 'csv',
|
|
142
|
+
csvDelimiter = ',',
|
|
143
|
+
shouldReplaceExisting = false,
|
|
144
|
+
languagePath = '',
|
|
145
|
+
isTest = false
|
|
146
|
+
} = {}) {
|
|
147
|
+
const translate = new Translate({
|
|
148
|
+
framework: this,
|
|
149
|
+
includedFilter,
|
|
150
|
+
masterLang,
|
|
151
|
+
targetLang,
|
|
152
|
+
format,
|
|
153
|
+
csvDelimiter,
|
|
154
|
+
shouldReplaceExisting,
|
|
155
|
+
jsonext: this.jsonext,
|
|
156
|
+
sourcePath: this.sourcePath,
|
|
157
|
+
languagePath,
|
|
158
|
+
outputPath: this.outputPath,
|
|
159
|
+
courseDir: this.courseDir,
|
|
160
|
+
useOutputData: this.useOutputData,
|
|
161
|
+
isTest,
|
|
162
|
+
log: this.log,
|
|
163
|
+
warn: this.warn
|
|
164
|
+
})
|
|
165
|
+
translate.load()
|
|
166
|
+
return translate
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** @returns {Framework} */
|
|
170
|
+
applyGlobalsDefaults ({
|
|
171
|
+
includedFilter = this.includedFilter,
|
|
172
|
+
useOutputData = this.useOutputData,
|
|
173
|
+
schemas = this.getSchemas({
|
|
174
|
+
includedFilter
|
|
175
|
+
}),
|
|
176
|
+
data = this.getData(useOutputData)
|
|
177
|
+
} = {}) {
|
|
178
|
+
const courseSchema = schemas.getCourseSchema()
|
|
179
|
+
data.languages.forEach(language => {
|
|
180
|
+
const { file, item: course } = language.getCourseFileItem()
|
|
181
|
+
course._globals = courseSchema.applyDefaults(course._globals, '_globals')
|
|
182
|
+
file.changed()
|
|
183
|
+
})
|
|
184
|
+
data.save()
|
|
185
|
+
return this
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** @returns {Framework} */
|
|
189
|
+
applyScreenSizeDefaults ({
|
|
190
|
+
includedFilter = this.includedFilter,
|
|
191
|
+
useOutputData = this.useOutputData,
|
|
192
|
+
schemas = this.getSchemas({
|
|
193
|
+
includedFilter
|
|
194
|
+
}),
|
|
195
|
+
data = this.getData(useOutputData)
|
|
196
|
+
} = {}) {
|
|
197
|
+
const configSchema = schemas.getConfigSchema()
|
|
198
|
+
const { file, item: config } = data.getConfigFileItem()
|
|
199
|
+
config.screenSize = configSchema.applyDefaults(config.screenSize, 'screenSize')
|
|
200
|
+
file.changed()
|
|
201
|
+
data.save()
|
|
202
|
+
return this
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Creates an includedFilter function based on config.build.includes and
|
|
207
|
+
* config.build.excludes from the course config.json. Automatically resolves
|
|
208
|
+
* plugin dependencies found in plugin bower.json / package.json files.
|
|
209
|
+
* @returns {function} A filter function: (sourcePath: string) => boolean
|
|
210
|
+
*/
|
|
211
|
+
makeIncludeFilter () {
|
|
212
|
+
const data = this.getData()
|
|
213
|
+
const configFileItem = data.getConfigFileItem()
|
|
214
|
+
const buildConfig = (configFileItem && configFileItem.item && configFileItem.item.build) || {}
|
|
215
|
+
const buildIncludes = buildConfig.includes && buildConfig.includes.length ? buildConfig.includes : null
|
|
216
|
+
const buildExcludes = buildConfig.excludes && buildConfig.excludes.length ? buildConfig.excludes : null
|
|
217
|
+
|
|
218
|
+
if (!buildIncludes && !buildExcludes) {
|
|
219
|
+
return function () { return true }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Resolve plugin dependencies for includes
|
|
223
|
+
let resolvedIncludes = null
|
|
224
|
+
if (buildIncludes) {
|
|
225
|
+
const allPlugins = this.getPlugins({ includedFilter: () => true })
|
|
226
|
+
const dependencies = []
|
|
227
|
+
|
|
228
|
+
allPlugins.plugins.forEach(plugin => {
|
|
229
|
+
if (!buildIncludes.includes(plugin.name)) return
|
|
230
|
+
const packageData = plugin.packageJSONFile.firstFileItem.item
|
|
231
|
+
const deps = packageData.dependencies
|
|
232
|
+
if (!deps || typeof deps !== 'object') return
|
|
233
|
+
Object.keys(deps).forEach(depName => {
|
|
234
|
+
if (!buildIncludes.includes(depName) && !dependencies.includes(depName)) {
|
|
235
|
+
dependencies.push(depName)
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
resolvedIncludes = [].concat(buildIncludes, dependencies)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Build regex patterns
|
|
244
|
+
const pluginTypes = ['components', 'extensions', 'menu', 'theme']
|
|
245
|
+
const sourcedir = 'src/'
|
|
246
|
+
|
|
247
|
+
let includedRegExp = null
|
|
248
|
+
let nestedIncludedRegExp = null
|
|
249
|
+
if (resolvedIncludes) {
|
|
250
|
+
const includePatterns = resolvedIncludes.map(plugin => {
|
|
251
|
+
return pluginTypes.map(type => sourcedir + type + '/' + plugin + '/').join('|')
|
|
252
|
+
}).join('|')
|
|
253
|
+
const corePattern = sourcedir + 'core/'
|
|
254
|
+
includedRegExp = new RegExp(corePattern + '|' + includePatterns, 'i')
|
|
255
|
+
|
|
256
|
+
const nestedPatterns = resolvedIncludes.map(plugin => {
|
|
257
|
+
return sourcedir + '([^/]*)/([^/]*)/' + 'less/plugins' + '/' + plugin + '/'
|
|
258
|
+
}).join('|')
|
|
259
|
+
nestedIncludedRegExp = new RegExp(nestedPatterns, 'i')
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let excludedRegExp = null
|
|
263
|
+
if (buildExcludes) {
|
|
264
|
+
const excludePatterns = buildExcludes.map(plugin => {
|
|
265
|
+
return pluginTypes.map(type => sourcedir + type + '/' + plugin + '/').join('|')
|
|
266
|
+
}).join('|')
|
|
267
|
+
excludedRegExp = new RegExp(excludePatterns, 'i')
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Return the filter closure
|
|
271
|
+
return function includedFilter (pluginPath) {
|
|
272
|
+
pluginPath = pluginPath.replace(/\\/g, '/')
|
|
273
|
+
|
|
274
|
+
const isIncluded = resolvedIncludes ? pluginPath.search(includedRegExp) !== -1 : undefined
|
|
275
|
+
const isExcluded = buildExcludes ? pluginPath.search(excludedRegExp) !== -1 : false
|
|
276
|
+
|
|
277
|
+
if (isExcluded || isIncluded === false) {
|
|
278
|
+
return false
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const nestedPluginsPath = !!pluginPath.match(/(?:.)+(?:\/less\/plugins)/g)
|
|
282
|
+
if (!nestedPluginsPath) {
|
|
283
|
+
return true
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (resolvedIncludes) {
|
|
287
|
+
return !!pluginPath.match(nestedIncludedRegExp)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return true
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export default Framework
|
package/lib/JSONFile.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import fs from 'fs-extra'
|
|
2
|
+
import JSONFileItem from './JSONFileItem.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {import('./Framework')} Framework
|
|
6
|
+
* @typedef {import('./JSONFile')} JSONFile
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* An abstraction for centralising the loading of JSON files, keeping track of
|
|
11
|
+
* sub-item changes and saving changed files.
|
|
12
|
+
*/
|
|
13
|
+
class JSONFile {
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} options
|
|
16
|
+
* @param {Framework} options.framework
|
|
17
|
+
* @param {string} options.path
|
|
18
|
+
* @param {Object|Array} options.data
|
|
19
|
+
* @param {boolean} options.hasChanged
|
|
20
|
+
*/
|
|
21
|
+
constructor ({
|
|
22
|
+
framework = null,
|
|
23
|
+
path = null,
|
|
24
|
+
jsonext = null,
|
|
25
|
+
data = null,
|
|
26
|
+
hasChanged = false
|
|
27
|
+
} = {}) {
|
|
28
|
+
/** @type {Framework} */
|
|
29
|
+
this.framework = framework
|
|
30
|
+
/** @type {string} */
|
|
31
|
+
this.path = path
|
|
32
|
+
/** @type {string} */
|
|
33
|
+
this.jsonext = jsonext
|
|
34
|
+
/** @type {Object|Array} */
|
|
35
|
+
this.data = data
|
|
36
|
+
/** @type {boolean} */
|
|
37
|
+
this.hasChanged = hasChanged
|
|
38
|
+
/** @type {[JSONFileItem]} */
|
|
39
|
+
this.fileItems = null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** @returns {JSONFile} */
|
|
43
|
+
load () {
|
|
44
|
+
this.fileItems = []
|
|
45
|
+
|
|
46
|
+
if (this.path) {
|
|
47
|
+
this.data = fs.readJSONSync(this.path)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const addObject = (item, index = null) => {
|
|
51
|
+
this.fileItems.push(new JSONFileItem({
|
|
52
|
+
framework: this.framework,
|
|
53
|
+
file: this,
|
|
54
|
+
item,
|
|
55
|
+
index
|
|
56
|
+
}))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (this.data instanceof Array) {
|
|
60
|
+
this.data.forEach((item, index) => addObject(item, index))
|
|
61
|
+
} else if (this.data instanceof Object) {
|
|
62
|
+
addObject(this.data, null)
|
|
63
|
+
} else {
|
|
64
|
+
const err = new Error(`Cannot load json file ${this.path} as it doesn't contain an Array or Object as its root`)
|
|
65
|
+
err.number = 10013
|
|
66
|
+
throw err
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return this
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* This is useful for files such as config.json or course.json which only have
|
|
74
|
+
* one item/object per file.
|
|
75
|
+
* @returns {JSONFileItem}
|
|
76
|
+
*/
|
|
77
|
+
get firstFileItem () {
|
|
78
|
+
return this.fileItems && this.fileItems[0]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Marks this file as having changed. This should be called after changing
|
|
83
|
+
* the fileItems contained in this instance.
|
|
84
|
+
* @returns {JSONFile}
|
|
85
|
+
*/
|
|
86
|
+
changed () {
|
|
87
|
+
this.hasChanged = true
|
|
88
|
+
return this
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Saves any fileItem changes to disk.
|
|
93
|
+
* @return {JSONFile}
|
|
94
|
+
*/
|
|
95
|
+
save () {
|
|
96
|
+
if (!this.hasChanged) {
|
|
97
|
+
return this
|
|
98
|
+
}
|
|
99
|
+
fs.writeJSONSync(this.path, this.data, { spaces: 2 })
|
|
100
|
+
return this
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export default JSONFile
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('./Framework')} Framework
|
|
3
|
+
* @typedef {import('./JSONFile')} JSONFile
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* An abstraction for carrying JSON sub-items with their file origins and locations.
|
|
8
|
+
*/
|
|
9
|
+
class JSONFileItem {
|
|
10
|
+
constructor ({
|
|
11
|
+
framework = null,
|
|
12
|
+
file = null,
|
|
13
|
+
item = null,
|
|
14
|
+
index = null
|
|
15
|
+
} = {}) {
|
|
16
|
+
/** @type {Framework} */
|
|
17
|
+
this.framework = framework
|
|
18
|
+
/** @type {JSONFile} */
|
|
19
|
+
this.file = file
|
|
20
|
+
/** @type {Object} */
|
|
21
|
+
this.item = item
|
|
22
|
+
/** @type {number} */
|
|
23
|
+
this.index = index
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default JSONFileItem
|
package/lib/Plugins.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import globs from 'globs'
|
|
2
|
+
import Plugin from './plugins/Plugin.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {import('./Framework')} Framework
|
|
6
|
+
* @typedef {import('./JSONFileItem')} JSONFileItem
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Represents all of the plugins in the src/ folder.
|
|
11
|
+
*/
|
|
12
|
+
class Plugins {
|
|
13
|
+
/**
|
|
14
|
+
* @param {Object} options
|
|
15
|
+
* @param {Framework} options.framework
|
|
16
|
+
* @param {function} options.includedFilter
|
|
17
|
+
* @param {string} options.sourcePath
|
|
18
|
+
* @param {function} options.log
|
|
19
|
+
* @param {function} options.warn
|
|
20
|
+
*/
|
|
21
|
+
constructor ({
|
|
22
|
+
framework = null,
|
|
23
|
+
includedFilter = function () { return true },
|
|
24
|
+
sourcePath = process.cwd() + '/src/',
|
|
25
|
+
courseDir = 'course',
|
|
26
|
+
log = console.log,
|
|
27
|
+
warn = console.warn
|
|
28
|
+
} = {}) {
|
|
29
|
+
/** @type {Framework} */
|
|
30
|
+
this.framework = framework
|
|
31
|
+
/** @type {function} */
|
|
32
|
+
this.includedFilter = includedFilter
|
|
33
|
+
/** @type {string} */
|
|
34
|
+
this.sourcePath = sourcePath
|
|
35
|
+
/** @type {string} */
|
|
36
|
+
this.courseDir = courseDir
|
|
37
|
+
/** @type {function} */
|
|
38
|
+
this.log = log
|
|
39
|
+
/** @type {function} */
|
|
40
|
+
this.warn = warn
|
|
41
|
+
/** @type {[Plugin]} */
|
|
42
|
+
this.plugins = []
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Returns the locations of all plugins in the src/ folder.
|
|
47
|
+
* @returns {[string]}
|
|
48
|
+
*/
|
|
49
|
+
get pluginLocations () {
|
|
50
|
+
return [
|
|
51
|
+
`${this.sourcePath}core/`,
|
|
52
|
+
`${this.sourcePath}!(core|${this.courseDir})/*/`
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** @returns {Plugins} */
|
|
57
|
+
load () {
|
|
58
|
+
this.plugins = globs.sync(this.pluginLocations)
|
|
59
|
+
.filter(sourcePath => this.includedFilter(sourcePath))
|
|
60
|
+
.map(sourcePath => {
|
|
61
|
+
const plugin = new Plugin({
|
|
62
|
+
framework: this.framework,
|
|
63
|
+
sourcePath,
|
|
64
|
+
log: this.log,
|
|
65
|
+
warn: this.warn
|
|
66
|
+
})
|
|
67
|
+
plugin.load()
|
|
68
|
+
return plugin
|
|
69
|
+
})
|
|
70
|
+
return this
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** @returns {JSONFileItem} */
|
|
74
|
+
getAllPackageJSONFileItems () {
|
|
75
|
+
return this.plugins.reduce((items, plugin) => {
|
|
76
|
+
items.push(...plugin.packageJSONFile.fileItems)
|
|
77
|
+
return items
|
|
78
|
+
}, [])
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default Plugins
|
package/lib/Schemas.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import fs from 'fs-extra'
|
|
4
|
+
import globs from 'globs'
|
|
5
|
+
import ExtensionSchema from './schema/ExtensionSchema.js'
|
|
6
|
+
import ModelSchema from './schema/ModelSchema.js'
|
|
7
|
+
import ModelSchemas from './schema/ModelSchemas.js'
|
|
8
|
+
import Plugins from './Plugins.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {import('./Framework')} Framework
|
|
12
|
+
* @typedef {import('./Plugins')} Plugins
|
|
13
|
+
* @typedef {import('./plugins/Plugin')} Plugin
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Represents all of the schemas in a course.
|
|
18
|
+
* @todo Work out how to do schema inheritance properly (i.e. component+accordion)
|
|
19
|
+
* @todo Stop deriving schema types (model/extension) from bower and/or folder paths
|
|
20
|
+
* @todo Stop deriving schema names from bower.json or filenames
|
|
21
|
+
* @todo Combining and applying multiple schemas for validation or defaults needs consideration
|
|
22
|
+
*/
|
|
23
|
+
class Schemas {
|
|
24
|
+
/**
|
|
25
|
+
* @param {Object} options
|
|
26
|
+
* @param {Framework} options.framework
|
|
27
|
+
* @param {function} options.includedFilter
|
|
28
|
+
* @param {Plugins} options.plugins
|
|
29
|
+
* @param {string} options.sourcePath
|
|
30
|
+
* @param {function} options.log
|
|
31
|
+
* @param {function} options.warn
|
|
32
|
+
*/
|
|
33
|
+
constructor ({
|
|
34
|
+
framework = null,
|
|
35
|
+
includedFilter = function () { return true },
|
|
36
|
+
plugins = null,
|
|
37
|
+
sourcePath = '',
|
|
38
|
+
log = console.log,
|
|
39
|
+
warn = console.warn
|
|
40
|
+
} = {}) {
|
|
41
|
+
/** @type {Framework} */
|
|
42
|
+
this.framework = framework
|
|
43
|
+
/** @type {function} */
|
|
44
|
+
this.includedFilter = includedFilter
|
|
45
|
+
/** @type {string} */
|
|
46
|
+
this.sourcePath = sourcePath.replace(/\\/g, '/')
|
|
47
|
+
/** @type {Plugins} */
|
|
48
|
+
this.plugins = plugins
|
|
49
|
+
/** @type {[Schema]]} */
|
|
50
|
+
this.schemas = null
|
|
51
|
+
/** @type {function} */
|
|
52
|
+
this.log = log
|
|
53
|
+
/** @type {function} */
|
|
54
|
+
this.warn = warn
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** @returns {Schemas} */
|
|
58
|
+
load () {
|
|
59
|
+
/**
|
|
60
|
+
* @param {Plugin} plugin
|
|
61
|
+
* @param {string} filePath
|
|
62
|
+
*/
|
|
63
|
+
const createSchema = (plugin, filePath) => {
|
|
64
|
+
const json = fs.readJSONSync(filePath)
|
|
65
|
+
const isExtensionSchema = Boolean(json.properties.pluginLocations)
|
|
66
|
+
const InferredSchemaClass = (isExtensionSchema ? ExtensionSchema : ModelSchema)
|
|
67
|
+
const inferredSchemaName = (plugin.name === 'core')
|
|
68
|
+
? path.parse(filePath).name.split('.')[0] // if core, get schema name from file name
|
|
69
|
+
: isExtensionSchema
|
|
70
|
+
? plugin.name // assume schema name is plugin name
|
|
71
|
+
: plugin.targetAttribute // assume schema name is plugin._[type] value
|
|
72
|
+
return new InferredSchemaClass({
|
|
73
|
+
name: inferredSchemaName,
|
|
74
|
+
plugin,
|
|
75
|
+
framework: this.framework,
|
|
76
|
+
filePath,
|
|
77
|
+
globalsType: plugin.type,
|
|
78
|
+
targetAttribute: plugin.targetAttribute
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.plugins = new Plugins({
|
|
83
|
+
framework: this.framework,
|
|
84
|
+
includedFilter: this.includedFilter,
|
|
85
|
+
sourcePath: this.sourcePath,
|
|
86
|
+
log: this.log,
|
|
87
|
+
warn: this.warn
|
|
88
|
+
})
|
|
89
|
+
this.plugins.load()
|
|
90
|
+
|
|
91
|
+
this.schemas = []
|
|
92
|
+
this.plugins.plugins.forEach(plugin => globs.sync(plugin.schemaLocations).forEach(filePath => {
|
|
93
|
+
const schema = createSchema(plugin, filePath)
|
|
94
|
+
schema.load()
|
|
95
|
+
this.schemas.push(schema)
|
|
96
|
+
}))
|
|
97
|
+
|
|
98
|
+
this.generateCourseGlobals()
|
|
99
|
+
this.generateModelExtensions()
|
|
100
|
+
|
|
101
|
+
return this
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Copy globals schema extensions from model/extension plugins to the course._globals
|
|
106
|
+
* schema.
|
|
107
|
+
* @returns {Schemas}
|
|
108
|
+
* @example
|
|
109
|
+
* courseModelSchema.properties._globals.properties._components.properties._accordion
|
|
110
|
+
*/
|
|
111
|
+
generateCourseGlobals () {
|
|
112
|
+
const courseSchema = this.getCourseSchema()
|
|
113
|
+
this.schemas.forEach(schema => {
|
|
114
|
+
const globalsPart = schema.getCourseGlobalsPart()
|
|
115
|
+
if (!globalsPart) {
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
_.merge(courseSchema.json.properties._globals.properties, globalsPart)
|
|
119
|
+
})
|
|
120
|
+
return this
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Copy pluginLocations schema extensions from the extension plugins to the appropriate model schemas
|
|
125
|
+
* @returns {Schemas}
|
|
126
|
+
* @example
|
|
127
|
+
* courseModelSchema.properties._assessment
|
|
128
|
+
* articleModelSchema.properties._trickle
|
|
129
|
+
* blockModelSchema.properties._trickle
|
|
130
|
+
*/
|
|
131
|
+
generateModelExtensions () {
|
|
132
|
+
const extensionSchemas = this.schemas.filter(schema => schema instanceof ExtensionSchema)
|
|
133
|
+
extensionSchemas.forEach(schema => {
|
|
134
|
+
const extensionParts = schema.getModelExtensionParts()
|
|
135
|
+
if (!extensionParts) {
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
for (const modelName in extensionParts) {
|
|
139
|
+
const extensionPart = extensionParts[modelName]
|
|
140
|
+
/**
|
|
141
|
+
* Check if the sub-schema part has any defined properties.
|
|
142
|
+
* A lot of extension schemas have empty objects with no properties.
|
|
143
|
+
*/
|
|
144
|
+
if (!extensionPart.properties) {
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
const modelSchema = this.getModelSchemaByName(modelName)
|
|
148
|
+
if (!modelSchema) {
|
|
149
|
+
const err = new Error(`Cannot add extensions to model which doesn't exits ${modelName}`)
|
|
150
|
+
err.number = 10012
|
|
151
|
+
throw err
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Notice that the targetAttribute is not used here, we allow the extension schema
|
|
155
|
+
* to define its own _[targetAttribute] to extend any core model.
|
|
156
|
+
*/
|
|
157
|
+
modelSchema.json.properties = _.merge({}, modelSchema.json.properties, extensionPart.properties)
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
return this
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @param {string} schemaName
|
|
165
|
+
* @returns {ModelSchema}
|
|
166
|
+
*/
|
|
167
|
+
getModelSchemaByName (schemaName) {
|
|
168
|
+
const modelSchemas = this.schemas.filter(schema => schema instanceof ModelSchema)
|
|
169
|
+
return modelSchemas.find(({ name }) => name === schemaName)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** @returns {ModelSchema} */
|
|
173
|
+
getCourseSchema () {
|
|
174
|
+
return this.getModelSchemaByName('course')
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** @returns {ModelSchema} */
|
|
178
|
+
getConfigSchema () {
|
|
179
|
+
return this.getModelSchemaByName('config')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Uses a model JSON to derive the appropriate schemas for the model.
|
|
184
|
+
* @param {Object} json
|
|
185
|
+
* @returns {ModelSchemas}
|
|
186
|
+
*/
|
|
187
|
+
getSchemasForModelJSON (json) {
|
|
188
|
+
const schemas = []
|
|
189
|
+
if (json._type) {
|
|
190
|
+
if (json._type === 'menu' || json._type === 'page') {
|
|
191
|
+
schemas.push(this.getModelSchemaByName('contentobject'))
|
|
192
|
+
}
|
|
193
|
+
schemas.push(this.getModelSchemaByName(json._type))
|
|
194
|
+
}
|
|
195
|
+
if (json._component) {
|
|
196
|
+
schemas.push(this.getModelSchemaByName(json._component))
|
|
197
|
+
}
|
|
198
|
+
if (json._model) {
|
|
199
|
+
schemas.push(this.getModelSchemaByName(json._model))
|
|
200
|
+
}
|
|
201
|
+
return new ModelSchemas({
|
|
202
|
+
framework: this.framework,
|
|
203
|
+
schemas: schemas.filter(Boolean)
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export default Schemas
|