@jseeio/jsee 0.3.7 → 0.3.8
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/.claude/settings.local.json +9 -0
- package/AGENTS.md +37 -0
- package/CHANGELOG.md +71 -0
- package/CLAUDE.md +5 -0
- package/README.md +20 -41
- package/bin/jsee +1 -1
- package/dist/jsee.js +1 -1
- package/dist/jsee.runtime.js +1 -1
- package/jest-puppeteer.config.js +2 -1
- package/jest.unit.config.js +8 -0
- package/load/index.html +9 -4
- package/package.json +15 -13
- package/src/app.js +35 -11
- package/src/cli.js +662 -548
- package/src/main.js +348 -152
- package/src/utils.js +590 -3
- package/src/worker.js +42 -18
- package/templates/bulma-app.vue +3 -2
- package/templates/bulma-input.vue +22 -18
- package/templates/bulma-output.vue +72 -7
- package/templates/common-inputs.js +2 -13
- package/templates/common-outputs.js +57 -2
- package/templates/file-picker-base.vue +169 -0
- package/templates/file-picker.vue +318 -0
- package/test/fixtures/lodash-like.js +15 -0
- package/test/fixtures/upload-sample.csv +3 -0
- package/test/test-basic.test.js +286 -11
- package/test/unit/utils.test.js +519 -0
- package/webpack.config.js +1 -0
package/src/utils.js
CHANGED
|
@@ -3,6 +3,421 @@ function isObject (item) {
|
|
|
3
3
|
return (typeof item === 'object' && !Array.isArray(item) && item !== null)
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
function shouldPreserveWorkerValue (value) {
|
|
7
|
+
if (!value || typeof value !== 'object') {
|
|
8
|
+
return true
|
|
9
|
+
}
|
|
10
|
+
if ((typeof File !== 'undefined') && (value instanceof File)) {
|
|
11
|
+
return true
|
|
12
|
+
}
|
|
13
|
+
if ((typeof Blob !== 'undefined') && (value instanceof Blob)) {
|
|
14
|
+
return true
|
|
15
|
+
}
|
|
16
|
+
if ((typeof ArrayBuffer !== 'undefined') && (value instanceof ArrayBuffer)) {
|
|
17
|
+
return true
|
|
18
|
+
}
|
|
19
|
+
if ((typeof ArrayBuffer !== 'undefined') && ArrayBuffer.isView && ArrayBuffer.isView(value)) {
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
if ((typeof Date !== 'undefined') && (value instanceof Date)) {
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
if ((typeof RegExp !== 'undefined') && (value instanceof RegExp)) {
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
if ((typeof URL !== 'undefined') && (value instanceof URL)) {
|
|
29
|
+
return true
|
|
30
|
+
}
|
|
31
|
+
if ((typeof Map !== 'undefined') && (value instanceof Map)) {
|
|
32
|
+
return true
|
|
33
|
+
}
|
|
34
|
+
if ((typeof Set !== 'undefined') && (value instanceof Set)) {
|
|
35
|
+
return true
|
|
36
|
+
}
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const VALID_INPUT_TYPES = [
|
|
41
|
+
'int',
|
|
42
|
+
'float',
|
|
43
|
+
'number',
|
|
44
|
+
'string',
|
|
45
|
+
'color',
|
|
46
|
+
'text',
|
|
47
|
+
'categorical',
|
|
48
|
+
'select',
|
|
49
|
+
'bool',
|
|
50
|
+
'checkbox',
|
|
51
|
+
'file',
|
|
52
|
+
'group',
|
|
53
|
+
'action',
|
|
54
|
+
'button'
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
const VALID_MODEL_TYPES = [
|
|
58
|
+
'function',
|
|
59
|
+
'class',
|
|
60
|
+
'async-init',
|
|
61
|
+
'async-function',
|
|
62
|
+
'get',
|
|
63
|
+
'post',
|
|
64
|
+
'py',
|
|
65
|
+
'tf'
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
function sanitizeName (inputName) {
|
|
69
|
+
return inputName.toLowerCase().replace(/[^a-z0-9_]/g, '_')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isWorkerInitMessage (data, initialized=false) {
|
|
73
|
+
if (initialized || !isObject(data)) {
|
|
74
|
+
return false
|
|
75
|
+
}
|
|
76
|
+
return (typeof data.url !== 'undefined') || (typeof data.code !== 'undefined')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getProgressState (value) {
|
|
80
|
+
if (value === null) {
|
|
81
|
+
return {
|
|
82
|
+
mode: 'indeterminate',
|
|
83
|
+
value: null
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const progressValue = Number(value)
|
|
88
|
+
if (Number.isNaN(progressValue)) {
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
mode: 'determinate',
|
|
94
|
+
value: Math.max(0, Math.min(100, progressValue))
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function shouldContinueInterval (interval, running, cancelled, caller) {
|
|
99
|
+
return Boolean(interval) && running && !cancelled && caller === 'run'
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function createAbortError (message='Operation aborted') {
|
|
103
|
+
const error = new Error(message)
|
|
104
|
+
error.name = 'AbortError'
|
|
105
|
+
return error
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isAbortRequested (signal, isCancelled) {
|
|
109
|
+
return !!(
|
|
110
|
+
(signal && signal.aborted)
|
|
111
|
+
|| (typeof isCancelled === 'function' && isCancelled())
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isFileLikeSource (source) {
|
|
116
|
+
return !!source
|
|
117
|
+
&& (typeof source === 'object')
|
|
118
|
+
&& (typeof source.slice === 'function')
|
|
119
|
+
&& (typeof source.size === 'number')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getUrlFromSource (source) {
|
|
123
|
+
if (typeof source === 'string') {
|
|
124
|
+
return source
|
|
125
|
+
}
|
|
126
|
+
if (source && source.kind === 'url' && (typeof source.url === 'string')) {
|
|
127
|
+
return source.url
|
|
128
|
+
}
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Async channel with backpressure for streaming chunks
|
|
133
|
+
function createChunkChannel () {
|
|
134
|
+
const queue = []
|
|
135
|
+
let waitingConsumer = null // resolve fn when consumer awaits data
|
|
136
|
+
let waitingProducer = null // resolve fn when producer awaits drain
|
|
137
|
+
let done = false
|
|
138
|
+
let error = null
|
|
139
|
+
const HIGH_WATER = 4
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
async push (chunk) {
|
|
143
|
+
if (done || error) return
|
|
144
|
+
queue.push(chunk)
|
|
145
|
+
if (waitingConsumer) {
|
|
146
|
+
const resolve = waitingConsumer
|
|
147
|
+
waitingConsumer = null
|
|
148
|
+
resolve()
|
|
149
|
+
}
|
|
150
|
+
if (queue.length >= HIGH_WATER) {
|
|
151
|
+
await new Promise(resolve => { waitingProducer = resolve })
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
close () {
|
|
155
|
+
done = true
|
|
156
|
+
if (waitingConsumer) {
|
|
157
|
+
const resolve = waitingConsumer
|
|
158
|
+
waitingConsumer = null
|
|
159
|
+
resolve()
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
fail (err) {
|
|
163
|
+
error = err
|
|
164
|
+
done = true
|
|
165
|
+
if (waitingConsumer) {
|
|
166
|
+
const resolve = waitingConsumer
|
|
167
|
+
waitingConsumer = null
|
|
168
|
+
resolve()
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
[Symbol.asyncIterator] () {
|
|
172
|
+
return {
|
|
173
|
+
async next () {
|
|
174
|
+
while (queue.length === 0 && !done) {
|
|
175
|
+
await new Promise(resolve => { waitingConsumer = resolve })
|
|
176
|
+
}
|
|
177
|
+
if (queue.length > 0) {
|
|
178
|
+
const value = queue.shift()
|
|
179
|
+
if (waitingProducer && queue.length < HIGH_WATER) {
|
|
180
|
+
const resolve = waitingProducer
|
|
181
|
+
waitingProducer = null
|
|
182
|
+
resolve()
|
|
183
|
+
}
|
|
184
|
+
return { value, done: false }
|
|
185
|
+
}
|
|
186
|
+
if (error) {
|
|
187
|
+
throw error
|
|
188
|
+
}
|
|
189
|
+
return { value: undefined, done: true }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Lightweight async-iterable reader for chunked data (File or fetch)
|
|
197
|
+
class ChunkedReader {
|
|
198
|
+
constructor (channel) {
|
|
199
|
+
this._channel = channel
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
[Symbol.asyncIterator] () {
|
|
203
|
+
return this._channel[Symbol.asyncIterator]()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async text () {
|
|
207
|
+
const decoder = new TextDecoder('utf-8')
|
|
208
|
+
let result = ''
|
|
209
|
+
for await (const chunk of this) {
|
|
210
|
+
result += decoder.decode(chunk, { stream: true })
|
|
211
|
+
}
|
|
212
|
+
result += decoder.decode()
|
|
213
|
+
return result
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async bytes () {
|
|
217
|
+
const parts = []
|
|
218
|
+
let totalLength = 0
|
|
219
|
+
for await (const chunk of this) {
|
|
220
|
+
parts.push(chunk)
|
|
221
|
+
totalLength += chunk.byteLength
|
|
222
|
+
}
|
|
223
|
+
const result = new Uint8Array(totalLength)
|
|
224
|
+
let offset = 0
|
|
225
|
+
for (const part of parts) {
|
|
226
|
+
result.set(part, offset)
|
|
227
|
+
offset += part.byteLength
|
|
228
|
+
}
|
|
229
|
+
return result
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async * lines () {
|
|
233
|
+
const decoder = new TextDecoder('utf-8')
|
|
234
|
+
let remainder = ''
|
|
235
|
+
for await (const chunk of this) {
|
|
236
|
+
remainder += decoder.decode(chunk, { stream: true })
|
|
237
|
+
const parts = remainder.split('\n')
|
|
238
|
+
remainder = parts.pop()
|
|
239
|
+
for (const line of parts) {
|
|
240
|
+
yield line
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
remainder += decoder.decode()
|
|
244
|
+
if (remainder.length > 0) {
|
|
245
|
+
yield remainder
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function createChunkedReader (producer) {
|
|
251
|
+
const channel = createChunkChannel()
|
|
252
|
+
|
|
253
|
+
Promise.resolve()
|
|
254
|
+
.then(() => producer(
|
|
255
|
+
chunk => channel.push(chunk),
|
|
256
|
+
() => channel.close(),
|
|
257
|
+
err => channel.fail(err)
|
|
258
|
+
))
|
|
259
|
+
.catch(err => channel.fail(err))
|
|
260
|
+
|
|
261
|
+
return new ChunkedReader(channel)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function createFileStream (source, options={}) {
|
|
265
|
+
const onProgress = options.onProgress
|
|
266
|
+
const signal = options.signal
|
|
267
|
+
const isCancelled = options.isCancelled
|
|
268
|
+
const chunkSize = (typeof options.chunkSize === 'number') && options.chunkSize > 0
|
|
269
|
+
? Math.floor(options.chunkSize)
|
|
270
|
+
: (256 * 1024)
|
|
271
|
+
|
|
272
|
+
return createChunkedReader(async (pushChunk, closeStream, failStream) => {
|
|
273
|
+
const totalBytes = source.size
|
|
274
|
+
let loadedBytes = 0
|
|
275
|
+
|
|
276
|
+
const reportProgress = async (value) => {
|
|
277
|
+
if (typeof onProgress === 'function') {
|
|
278
|
+
await onProgress(value)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
await reportProgress(totalBytes > 0 ? 0 : null)
|
|
284
|
+
while (loadedBytes < totalBytes) {
|
|
285
|
+
if (isAbortRequested(signal, isCancelled)) {
|
|
286
|
+
throw createAbortError('createFileStream: aborted')
|
|
287
|
+
}
|
|
288
|
+
const nextOffset = Math.min(loadedBytes + chunkSize, totalBytes)
|
|
289
|
+
const blob = source.slice(loadedBytes, nextOffset)
|
|
290
|
+
const value = new Uint8Array(await blob.arrayBuffer())
|
|
291
|
+
loadedBytes = nextOffset
|
|
292
|
+
if (value.byteLength > 0) {
|
|
293
|
+
await pushChunk(value)
|
|
294
|
+
}
|
|
295
|
+
const progressValue = totalBytes > 0
|
|
296
|
+
? Math.round((loadedBytes / totalBytes) * 100)
|
|
297
|
+
: null
|
|
298
|
+
await reportProgress(progressValue)
|
|
299
|
+
}
|
|
300
|
+
await reportProgress(totalBytes > 0 ? 100 : null)
|
|
301
|
+
closeStream()
|
|
302
|
+
} catch (error) {
|
|
303
|
+
failStream(error)
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function createFetchStream (source, options={}) {
|
|
309
|
+
const sourceUrl = getUrlFromSource(source)
|
|
310
|
+
if (!sourceUrl) {
|
|
311
|
+
throw new Error('createFetchStream: unsupported source type')
|
|
312
|
+
}
|
|
313
|
+
const onProgress = options.onProgress
|
|
314
|
+
const signal = options.signal
|
|
315
|
+
const isCancelled = options.isCancelled
|
|
316
|
+
const fetchImpl = options.fetch || (typeof fetch === 'function' ? fetch : null)
|
|
317
|
+
if (!fetchImpl) {
|
|
318
|
+
throw new Error('createFetchStream: fetch is not available')
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return createChunkedReader(async (pushChunk, closeStream, failStream) => {
|
|
322
|
+
const reportProgress = async (value) => {
|
|
323
|
+
if (typeof onProgress === 'function') {
|
|
324
|
+
await onProgress(value)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const abortController = typeof AbortController !== 'undefined'
|
|
329
|
+
? new AbortController()
|
|
330
|
+
: null
|
|
331
|
+
const fetchSignal = abortController ? abortController.signal : signal
|
|
332
|
+
if (signal && abortController) {
|
|
333
|
+
if (signal.aborted) {
|
|
334
|
+
abortController.abort()
|
|
335
|
+
} else {
|
|
336
|
+
signal.addEventListener('abort', () => abortController.abort(), { once: true })
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
let reader
|
|
341
|
+
try {
|
|
342
|
+
if (isAbortRequested(signal, isCancelled)) {
|
|
343
|
+
throw createAbortError('createFetchStream: aborted before fetch')
|
|
344
|
+
}
|
|
345
|
+
const response = await fetchImpl(sourceUrl, fetchSignal ? { signal: fetchSignal } : {})
|
|
346
|
+
if (!response.ok) {
|
|
347
|
+
throw new Error(`createFetchStream: failed to fetch ${sourceUrl} (${response.status})`)
|
|
348
|
+
}
|
|
349
|
+
if (!response.body) {
|
|
350
|
+
throw new Error(`createFetchStream: empty response body for ${sourceUrl}`)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const totalBytesHeader = response.headers.get('content-length')
|
|
354
|
+
const totalBytes = totalBytesHeader ? Number(totalBytesHeader) : null
|
|
355
|
+
const hasKnownLength = !!totalBytes && !Number.isNaN(totalBytes) && totalBytes > 0
|
|
356
|
+
let loadedBytes = 0
|
|
357
|
+
reader = response.body.getReader()
|
|
358
|
+
|
|
359
|
+
await reportProgress(hasKnownLength ? 0 : null)
|
|
360
|
+
while (true) {
|
|
361
|
+
if (isAbortRequested(signal, isCancelled)) {
|
|
362
|
+
if (abortController) {
|
|
363
|
+
abortController.abort()
|
|
364
|
+
}
|
|
365
|
+
throw createAbortError('createFetchStream: aborted during read')
|
|
366
|
+
}
|
|
367
|
+
const { done, value } = await reader.read()
|
|
368
|
+
if (done) {
|
|
369
|
+
break
|
|
370
|
+
}
|
|
371
|
+
loadedBytes += value.byteLength
|
|
372
|
+
if (value.byteLength > 0) {
|
|
373
|
+
await pushChunk(value)
|
|
374
|
+
}
|
|
375
|
+
const progressValue = hasKnownLength
|
|
376
|
+
? Math.round((loadedBytes / totalBytes) * 100)
|
|
377
|
+
: null
|
|
378
|
+
await reportProgress(progressValue)
|
|
379
|
+
}
|
|
380
|
+
await reportProgress(hasKnownLength ? 100 : null)
|
|
381
|
+
closeStream()
|
|
382
|
+
} catch (error) {
|
|
383
|
+
failStream(error)
|
|
384
|
+
} finally {
|
|
385
|
+
if (reader) {
|
|
386
|
+
reader.releaseLock()
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function wrapStreamInputs (inputs, streamConfig={}, options={}) {
|
|
393
|
+
if (!isObject(inputs)) {
|
|
394
|
+
return inputs
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const wrapped = Object.assign({}, inputs)
|
|
398
|
+
Object.keys(streamConfig).forEach((inputName) => {
|
|
399
|
+
const config = streamConfig[inputName]
|
|
400
|
+
if (!config || config.stream !== true) {
|
|
401
|
+
return
|
|
402
|
+
}
|
|
403
|
+
if (typeof wrapped[inputName] === 'undefined' || wrapped[inputName] === null) {
|
|
404
|
+
return
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const source = wrapped[inputName]
|
|
408
|
+
if (isFileLikeSource(source)) {
|
|
409
|
+
wrapped[inputName] = createFileStream(source, options)
|
|
410
|
+
return
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const sourceUrl = getUrlFromSource(source)
|
|
414
|
+
if (sourceUrl) {
|
|
415
|
+
wrapped[inputName] = createFetchStream(sourceUrl, options)
|
|
416
|
+
}
|
|
417
|
+
})
|
|
418
|
+
return wrapped
|
|
419
|
+
}
|
|
420
|
+
|
|
6
421
|
async function getModelFuncJS (model, target, app) {
|
|
7
422
|
let modelFunc
|
|
8
423
|
switch (model.type) {
|
|
@@ -82,7 +497,7 @@ function importScriptAsync (imp, async=true) {
|
|
|
82
497
|
console.log('Script loaded from cache:', ev.detail.url)
|
|
83
498
|
resolve({ status: true })
|
|
84
499
|
})
|
|
85
|
-
document.body.appendChild(scriptElement)
|
|
500
|
+
document.body.appendChild(scriptElement)
|
|
86
501
|
document.body.appendChild(eventElement)
|
|
87
502
|
} else {
|
|
88
503
|
// Create script element from import.url
|
|
@@ -95,7 +510,7 @@ function importScriptAsync (imp, async=true) {
|
|
|
95
510
|
scriptElement.addEventListener('error', (ev) => {
|
|
96
511
|
reject({
|
|
97
512
|
status: false,
|
|
98
|
-
message: `Failed to import
|
|
513
|
+
message: `Failed to import ${imp.url}`
|
|
99
514
|
})
|
|
100
515
|
})
|
|
101
516
|
document.body.appendChild(scriptElement);
|
|
@@ -146,6 +561,167 @@ async function delay (ms) {
|
|
|
146
561
|
return new Promise(resolve => setTimeout(resolve, ms || 1))
|
|
147
562
|
}
|
|
148
563
|
|
|
564
|
+
function toWorkerSerializable (value) {
|
|
565
|
+
if (shouldPreserveWorkerValue(value)) {
|
|
566
|
+
return value
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (Array.isArray(value)) {
|
|
570
|
+
return value.map(item => toWorkerSerializable(item))
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (!isObject(value)) {
|
|
574
|
+
return value
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const copy = {}
|
|
578
|
+
Object.keys(value).forEach(key => {
|
|
579
|
+
copy[key] = toWorkerSerializable(value[key])
|
|
580
|
+
})
|
|
581
|
+
return copy
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Simple debounce to prevent rapid-fire calls (e.g. autorun on every keystroke)
|
|
585
|
+
function debounce (fn, ms) {
|
|
586
|
+
let timer
|
|
587
|
+
return function (...args) {
|
|
588
|
+
clearTimeout(timer)
|
|
589
|
+
timer = setTimeout(() => fn.apply(this, args), ms)
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Extract function name from code (string or function reference).
|
|
594
|
+
// Handles 'function name', 'async function name'. Arrow/anonymous return undefined.
|
|
595
|
+
function getName (code) {
|
|
596
|
+
switch (typeof code) {
|
|
597
|
+
case 'function':
|
|
598
|
+
return code.name
|
|
599
|
+
case 'string':
|
|
600
|
+
const words = code.split(' ')
|
|
601
|
+
const functionIndex = words.findIndex((word) => word === 'function')
|
|
602
|
+
if (functionIndex === -1) return undefined
|
|
603
|
+
const name = words[functionIndex + 1]
|
|
604
|
+
if (!name || name.includes('(')) return undefined
|
|
605
|
+
return name
|
|
606
|
+
default:
|
|
607
|
+
return undefined
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function validateInputSchema (input, path, report) {
|
|
612
|
+
if (!isObject(input)) {
|
|
613
|
+
report.errors.push(`${path} should be an object`)
|
|
614
|
+
return
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if ((typeof input.name !== 'undefined') && (typeof input.name !== 'string')) {
|
|
618
|
+
report.warnings.push(`${path}.name should be a string`)
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if ((typeof input.type !== 'undefined') && !VALID_INPUT_TYPES.includes(input.type)) {
|
|
622
|
+
report.warnings.push(`${path}.type '${input.type}' is not recognized`)
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if ((typeof input.raw !== 'undefined') && (typeof input.raw !== 'boolean')) {
|
|
626
|
+
report.warnings.push(`${path}.raw should be a boolean`)
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if ((typeof input.stream !== 'undefined') && (typeof input.stream !== 'boolean')) {
|
|
630
|
+
report.warnings.push(`${path}.stream should be a boolean`)
|
|
631
|
+
}
|
|
632
|
+
if ((input.stream === true) && (input.type !== 'file')) {
|
|
633
|
+
report.warnings.push(`${path}.stream is supported only for file inputs`)
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (input.type === 'group') {
|
|
637
|
+
if (!Array.isArray(input.elements)) {
|
|
638
|
+
report.warnings.push(`${path}.elements should be an array for group inputs`)
|
|
639
|
+
} else {
|
|
640
|
+
input.elements.forEach((element, index) => {
|
|
641
|
+
validateInputSchema(element, `${path}.elements[${index}]`, report)
|
|
642
|
+
})
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (typeof input.alias !== 'undefined') {
|
|
647
|
+
const validAlias = (
|
|
648
|
+
typeof input.alias === 'string'
|
|
649
|
+
|| (Array.isArray(input.alias) && input.alias.every(alias => typeof alias === 'string'))
|
|
650
|
+
)
|
|
651
|
+
if (!validAlias) {
|
|
652
|
+
report.warnings.push(`${path}.alias should be a string or an array of strings`)
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function validateModelSchema (model, path, report) {
|
|
658
|
+
if (typeof model === 'function') {
|
|
659
|
+
return
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (!isObject(model)) {
|
|
663
|
+
report.errors.push(`${path} should be an object or function`)
|
|
664
|
+
return
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if ((typeof model.type !== 'undefined') && !VALID_MODEL_TYPES.includes(model.type)) {
|
|
668
|
+
report.warnings.push(`${path}.type '${model.type}' is not recognized`)
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if ((typeof model.worker !== 'undefined') && (typeof model.worker !== 'boolean')) {
|
|
672
|
+
report.warnings.push(`${path}.worker should be a boolean`)
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (typeof model.timeout !== 'undefined') {
|
|
676
|
+
const timeoutValid = (typeof model.timeout === 'number') && !Number.isNaN(model.timeout) && (model.timeout > 0)
|
|
677
|
+
if (!timeoutValid) {
|
|
678
|
+
report.warnings.push(`${path}.timeout should be a positive number`)
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function validateSchema (schema) {
|
|
684
|
+
const report = {
|
|
685
|
+
errors: [],
|
|
686
|
+
warnings: []
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (!isObject(schema)) {
|
|
690
|
+
report.errors.push('Schema should be an object')
|
|
691
|
+
return report
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const hasModel = typeof schema.model !== 'undefined'
|
|
695
|
+
const hasView = (typeof schema.view !== 'undefined') || (typeof schema.render !== 'undefined')
|
|
696
|
+
|
|
697
|
+
if (!hasModel && !hasView) {
|
|
698
|
+
report.errors.push('Schema should define `model` (or `view`/`render`)')
|
|
699
|
+
} else if (!hasModel && hasView) {
|
|
700
|
+
report.warnings.push('Schema has no `model`, using `view`/`render` only')
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (typeof schema.inputs !== 'undefined') {
|
|
704
|
+
if (!Array.isArray(schema.inputs)) {
|
|
705
|
+
report.errors.push('`inputs` should be an array')
|
|
706
|
+
} else {
|
|
707
|
+
schema.inputs.forEach((input, index) => {
|
|
708
|
+
validateInputSchema(input, `inputs[${index}]`, report)
|
|
709
|
+
})
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (hasModel) {
|
|
714
|
+
const models = Array.isArray(schema.model)
|
|
715
|
+
? schema.model
|
|
716
|
+
: [schema.model]
|
|
717
|
+
models.forEach((model, index) => {
|
|
718
|
+
validateModelSchema(model, `model[${index}]`, report)
|
|
719
|
+
})
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return report
|
|
723
|
+
}
|
|
724
|
+
|
|
149
725
|
module.exports = {
|
|
150
726
|
isObject,
|
|
151
727
|
loadFromDOM,
|
|
@@ -153,5 +729,16 @@ module.exports = {
|
|
|
153
729
|
getModelFuncAPI,
|
|
154
730
|
importScripts,
|
|
155
731
|
getUrl,
|
|
156
|
-
delay
|
|
732
|
+
delay,
|
|
733
|
+
debounce,
|
|
734
|
+
sanitizeName,
|
|
735
|
+
isWorkerInitMessage,
|
|
736
|
+
getProgressState,
|
|
737
|
+
shouldContinueInterval,
|
|
738
|
+
createFileStream,
|
|
739
|
+
createFetchStream,
|
|
740
|
+
wrapStreamInputs,
|
|
741
|
+
getName,
|
|
742
|
+
validateSchema,
|
|
743
|
+
toWorkerSerializable
|
|
157
744
|
}
|