@jseeio/jsee 0.8.2 → 0.8.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/CHANGELOG.md +12 -0
- package/README.md +17 -7
- package/dist/jsee.core.js +1 -1
- package/dist/jsee.full.js +1 -1
- package/dist/jsee.runtime.js +1 -1
- package/package.json +1 -1
- package/src/cli.js +711 -49
- package/src/main.js +5 -10
- package/src/utils.js +38 -0
- package/templates/minimal-app.vue +2 -1
package/src/cli.js
CHANGED
|
@@ -2,10 +2,11 @@ const fs = require('fs')
|
|
|
2
2
|
const path = require('path')
|
|
3
3
|
const os = require('os')
|
|
4
4
|
const crypto = require('crypto')
|
|
5
|
+
const Module = require('module')
|
|
5
6
|
|
|
6
7
|
const minimist = require('minimist')
|
|
7
8
|
|
|
8
|
-
const { getModelFuncJS, sanitizeName, generateOpenAPISpec, serializeResult, parseMultipart, fileExtToOutputType } = require('./utils.js')
|
|
9
|
+
const { getModelFuncJS, sanitizeName, isRecordObject, normalizeFileOutputValue, generateOpenAPISpec, serializeResult, parseMultipart, fileExtToOutputType } = require('./utils.js')
|
|
9
10
|
|
|
10
11
|
let jsdoc2md
|
|
11
12
|
function getJsdocToMarkdown () {
|
|
@@ -69,6 +70,137 @@ function readDirListing (resolved, relative) {
|
|
|
69
70
|
}))
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
function isPackageSpecifier (value) {
|
|
74
|
+
if (typeof value !== 'string') return false
|
|
75
|
+
if (!value || value.startsWith('.') || value.startsWith('/') || value.startsWith('file:')) return false
|
|
76
|
+
if (isHttpUrl(value)) return false
|
|
77
|
+
if (value.includes('\\')) return false
|
|
78
|
+
if (value.startsWith('@')) {
|
|
79
|
+
return /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(value)
|
|
80
|
+
}
|
|
81
|
+
return /^[A-Za-z0-9._-]+$/.test(value)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function looksLikeMissingPackageInput (value, cwd) {
|
|
85
|
+
if (!isPackageSpecifier(value)) return false
|
|
86
|
+
if (value.includes('.json') || value.includes('.js')) return false
|
|
87
|
+
return !fs.existsSync(path.resolve(cwd, value))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getPackageInputInstallHint (specifier) {
|
|
91
|
+
return `npx -p @jseeio/jsee -p ${specifier} jsee ${specifier} --serve`
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolvePackageJsonPath (specifier, cwd) {
|
|
95
|
+
const resolver = Module.createRequire(path.join(cwd, '__jsee_resolve__.js'))
|
|
96
|
+
const searchPaths = resolver.resolve.paths(specifier) || []
|
|
97
|
+
|
|
98
|
+
for (const base of searchPaths) {
|
|
99
|
+
const candidate = path.join(base, specifier, 'package.json')
|
|
100
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
return require.resolve(`${specifier}/package.json`, { paths: [cwd] })
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function readJseePackageInput (specifier, packageJsonPath) {
|
|
111
|
+
const packageRoot = path.dirname(packageJsonPath)
|
|
112
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
|
113
|
+
const appConfig = packageJson.jsee
|
|
114
|
+
let schemaRel
|
|
115
|
+
let descriptionRel
|
|
116
|
+
|
|
117
|
+
if (typeof appConfig === 'string') {
|
|
118
|
+
schemaRel = appConfig
|
|
119
|
+
} else if (appConfig && typeof appConfig === 'object') {
|
|
120
|
+
schemaRel = appConfig.schema || appConfig.input || appConfig.inputs
|
|
121
|
+
descriptionRel = appConfig.description
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!schemaRel) {
|
|
125
|
+
throw new Error(`${specifier} does not declare a JSEE app. Add "jsee": "schema.json" to its package.json.`)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const schemaPath = path.resolve(packageRoot, schemaRel)
|
|
129
|
+
if (!fs.existsSync(schemaPath) || !fs.statSync(schemaPath).isFile()) {
|
|
130
|
+
throw new Error(`${specifier} declares JSEE schema ${schemaRel}, but the file was not found.`)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let descriptionPath = null
|
|
134
|
+
if (descriptionRel) {
|
|
135
|
+
descriptionPath = path.resolve(packageRoot, descriptionRel)
|
|
136
|
+
} else {
|
|
137
|
+
const readmePath = path.join(packageRoot, 'README.md')
|
|
138
|
+
if (fs.existsSync(readmePath) && fs.statSync(readmePath).isFile()) descriptionPath = readmePath
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
packageName: packageJson.name || specifier,
|
|
143
|
+
packageRoot,
|
|
144
|
+
schemaPath,
|
|
145
|
+
descriptionPath
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function resolveJseePackageInput (specifier, cwd) {
|
|
150
|
+
if (!isPackageSpecifier(specifier)) return null
|
|
151
|
+
|
|
152
|
+
const packageJsonPath = resolvePackageJsonPath(specifier, cwd)
|
|
153
|
+
if (!packageJsonPath) return null
|
|
154
|
+
|
|
155
|
+
return readJseePackageInput(specifier, packageJsonPath)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function findPackageRoot (startDir) {
|
|
159
|
+
let dir = path.resolve(startDir)
|
|
160
|
+
if (fs.existsSync(dir) && fs.statSync(dir).isFile()) dir = path.dirname(dir)
|
|
161
|
+
|
|
162
|
+
while (true) {
|
|
163
|
+
const packageJsonPath = path.join(dir, 'package.json')
|
|
164
|
+
if (fs.existsSync(packageJsonPath) && fs.statSync(packageJsonPath).isFile()) return dir
|
|
165
|
+
|
|
166
|
+
const parent = path.dirname(dir)
|
|
167
|
+
if (parent === dir) throw new Error(`No package.json found from ${startDir}`)
|
|
168
|
+
dir = parent
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function hasArgOption (args, names) {
|
|
173
|
+
return args.some(arg => names.some(name => arg === name || arg.startsWith(name + '=')))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function runPackage (dirname, args=[]) {
|
|
177
|
+
if (!Array.isArray(args)) throw new Error('runPackage args must be an array')
|
|
178
|
+
|
|
179
|
+
const packageRoot = findPackageRoot(dirname)
|
|
180
|
+
const packageInput = readJseePackageInput(packageRoot, path.join(packageRoot, 'package.json'))
|
|
181
|
+
const pargv = []
|
|
182
|
+
|
|
183
|
+
if (!hasArgOption(args, ['--inputs', '-i'])) {
|
|
184
|
+
pargv.push('--inputs', packageInput.schemaPath)
|
|
185
|
+
}
|
|
186
|
+
if (!hasArgOption(args, ['--description', '-d']) && packageInput.descriptionPath) {
|
|
187
|
+
pargv.push('--description', packageInput.descriptionPath)
|
|
188
|
+
}
|
|
189
|
+
pargv.push(...args)
|
|
190
|
+
|
|
191
|
+
if (hasArgOption(args, ['--run'])) {
|
|
192
|
+
return await gen(pargv)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const previousCwd = process.cwd()
|
|
196
|
+
process.chdir(packageInput.packageRoot)
|
|
197
|
+
try {
|
|
198
|
+
return await gen(pargv)
|
|
199
|
+
} finally {
|
|
200
|
+
process.chdir(previousCwd)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
72
204
|
function generateIdentitySchema (target, cwd) {
|
|
73
205
|
const resolved = path.isAbsolute(target) ? target : path.join(cwd, target)
|
|
74
206
|
const stat = fs.existsSync(resolved) ? fs.statSync(resolved) : null
|
|
@@ -259,6 +391,402 @@ function resolveOutputPath (cwd, outputPath) {
|
|
|
259
391
|
return path.join(cwd, outputPath)
|
|
260
392
|
}
|
|
261
393
|
|
|
394
|
+
function writeOutputFile (cwd, outputPath, content) {
|
|
395
|
+
const resolved = resolveOutputPath(cwd, outputPath)
|
|
396
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true })
|
|
397
|
+
fs.writeFileSync(resolved, content)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Run mode helpers
|
|
401
|
+
function getInputDefaultValue (input) {
|
|
402
|
+
if (!input || typeof input !== 'object') return undefined
|
|
403
|
+
if (input.type === 'group' && Array.isArray(input.elements)) {
|
|
404
|
+
const value = {}
|
|
405
|
+
input.elements.forEach(element => {
|
|
406
|
+
const elementValue = getInputDefaultValue(element)
|
|
407
|
+
if (typeof elementValue !== 'undefined' && element.name) value[element.name] = elementValue
|
|
408
|
+
})
|
|
409
|
+
return value
|
|
410
|
+
}
|
|
411
|
+
if (Object.prototype.hasOwnProperty.call(input, 'default')) return input.default
|
|
412
|
+
|
|
413
|
+
switch (input.type) {
|
|
414
|
+
case 'int':
|
|
415
|
+
case 'float':
|
|
416
|
+
case 'number':
|
|
417
|
+
return 0
|
|
418
|
+
case 'string':
|
|
419
|
+
case 'text':
|
|
420
|
+
return ''
|
|
421
|
+
case 'color':
|
|
422
|
+
return '#000000'
|
|
423
|
+
case 'categorical':
|
|
424
|
+
case 'select':
|
|
425
|
+
case 'radio':
|
|
426
|
+
return input.options && input.options.length ? input.options[0] : ''
|
|
427
|
+
case 'bool':
|
|
428
|
+
case 'checkbox':
|
|
429
|
+
case 'toggle':
|
|
430
|
+
return false
|
|
431
|
+
case 'multi-select':
|
|
432
|
+
return []
|
|
433
|
+
case 'range':
|
|
434
|
+
return [input.min || 0, input.max || 100]
|
|
435
|
+
case 'slider':
|
|
436
|
+
return input.min || 0
|
|
437
|
+
case 'folder':
|
|
438
|
+
return input.default || []
|
|
439
|
+
case 'file':
|
|
440
|
+
return ''
|
|
441
|
+
default:
|
|
442
|
+
return undefined
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function getSchemaInputDefaults (schema) {
|
|
447
|
+
const data = {}
|
|
448
|
+
if (!schema.inputs) return data
|
|
449
|
+
schema.inputs.forEach(input => {
|
|
450
|
+
if (!input.name) return
|
|
451
|
+
const value = getInputDefaultValue(input)
|
|
452
|
+
if (typeof value !== 'undefined') data[input.name] = value
|
|
453
|
+
})
|
|
454
|
+
return data
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function loadBrowserStyleModelFunction (src, model, modelPath) {
|
|
458
|
+
const modelName = getModelExportName(model)
|
|
459
|
+
const localRequire = modelPath
|
|
460
|
+
? Module.createRequire(modelPath)
|
|
461
|
+
: require
|
|
462
|
+
const moduleShim = { exports: {} }
|
|
463
|
+
const dirname = modelPath ? path.dirname(modelPath) : process.cwd()
|
|
464
|
+
const filename = modelPath || path.join(dirname, '__jsee_model__.js')
|
|
465
|
+
|
|
466
|
+
const fn = new Function(
|
|
467
|
+
'require',
|
|
468
|
+
'module',
|
|
469
|
+
'exports',
|
|
470
|
+
'__filename',
|
|
471
|
+
'__dirname',
|
|
472
|
+
`${src}
|
|
473
|
+
return typeof ${toJsIdentifier(modelName)} === 'function' ? ${toJsIdentifier(modelName)} : (module.exports && (module.exports.default || module.exports[${JSON.stringify(modelName)}] || module.exports))`
|
|
474
|
+
)
|
|
475
|
+
const target = fn(localRequire, moduleShim, moduleShim.exports, filename, dirname)
|
|
476
|
+
if (typeof target === 'function') return target
|
|
477
|
+
if (target && typeof target === 'object') {
|
|
478
|
+
if (typeof target[modelName] === 'function') return target[modelName]
|
|
479
|
+
if (typeof target.default === 'function') return target.default
|
|
480
|
+
}
|
|
481
|
+
return target
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function resolveExportedTarget (target, model) {
|
|
485
|
+
const modelName = getModelExportName(model)
|
|
486
|
+
if (typeof target === 'function') return target
|
|
487
|
+
if (target && typeof target === 'object') {
|
|
488
|
+
if (typeof target[modelName] === 'function') return target[modelName]
|
|
489
|
+
if (typeof target.default === 'function') return target.default
|
|
490
|
+
}
|
|
491
|
+
return target
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function loadCliModelFunction (model, schemaPath, cwd, log) {
|
|
495
|
+
if (model.type === 'get' || model.type === 'post') {
|
|
496
|
+
throw new Error(`--run cannot execute remote ${model.type.toUpperCase()} model ${model.name || model.url}. Use a local JavaScript model.`)
|
|
497
|
+
}
|
|
498
|
+
if (model.type === 'py') {
|
|
499
|
+
throw new Error('--run currently supports local JavaScript models. Python model execution is available through the dev server/API path.')
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
let target
|
|
503
|
+
let modelPath = null
|
|
504
|
+
|
|
505
|
+
if (typeof model.code === 'function') {
|
|
506
|
+
target = model.code
|
|
507
|
+
} else if (typeof model.code === 'string' && model.code.trim()) {
|
|
508
|
+
try {
|
|
509
|
+
target = loadBrowserStyleModelFunction(model.code, model, null)
|
|
510
|
+
} catch (error) {
|
|
511
|
+
target = undefined
|
|
512
|
+
}
|
|
513
|
+
if (typeof target !== 'function') {
|
|
514
|
+
try {
|
|
515
|
+
target = new Function(`return (${model.code})`)()
|
|
516
|
+
} catch (error) {
|
|
517
|
+
// Keep the clearer error below.
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
} else if (model.url && !isHttpUrl(model.url)) {
|
|
521
|
+
modelPath = path.resolve(schemaPath ? path.dirname(schemaPath) : cwd, model.url)
|
|
522
|
+
target = resolveExportedTarget(require(modelPath), model)
|
|
523
|
+
if (typeof target !== 'function' && (!target || Object.keys(target).length === 0)) {
|
|
524
|
+
const src = fs.readFileSync(modelPath, 'utf-8')
|
|
525
|
+
target = loadBrowserStyleModelFunction(src, model, modelPath)
|
|
526
|
+
}
|
|
527
|
+
} else {
|
|
528
|
+
throw new Error(`--run requires local model code or a local model url for ${model.name || 'model'}`)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
target = resolveExportedTarget(target, model)
|
|
532
|
+
if (typeof target !== 'function') {
|
|
533
|
+
throw new Error(`Could not load a callable function for model ${model.name || model.url || 'model'}`)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return getModelFuncJS(model, target, {
|
|
537
|
+
log,
|
|
538
|
+
progress: () => {},
|
|
539
|
+
isCancelled: () => false
|
|
540
|
+
})
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async function runSchemaOnce (schema, data, schemaPath, cwd, log) {
|
|
544
|
+
let result = data
|
|
545
|
+
for (const model of schema.model) {
|
|
546
|
+
const modelFunc = await loadCliModelFunction(model, schemaPath, cwd, log)
|
|
547
|
+
const next = await modelFunc(result)
|
|
548
|
+
if (isRecordObject(result) && isRecordObject(next)) {
|
|
549
|
+
result = Object.assign({}, result, next)
|
|
550
|
+
} else if (typeof next !== 'undefined') {
|
|
551
|
+
result = next
|
|
552
|
+
}
|
|
553
|
+
if (isRecordObject(result) && result.stop) break
|
|
554
|
+
}
|
|
555
|
+
return result
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function flattenOutputs (outputs, flat=[]) {
|
|
559
|
+
if (!Array.isArray(outputs)) return flat
|
|
560
|
+
outputs.forEach(output => {
|
|
561
|
+
if (output && output.type === 'group' && Array.isArray(output.elements)) {
|
|
562
|
+
flattenOutputs(output.elements, flat)
|
|
563
|
+
} else if (output) {
|
|
564
|
+
flat.push(output)
|
|
565
|
+
}
|
|
566
|
+
})
|
|
567
|
+
return flat
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function cleanRunResultObject (result) {
|
|
571
|
+
if (!isRecordObject(result)) return result
|
|
572
|
+
const clean = Object.assign({}, result)
|
|
573
|
+
delete clean.caller
|
|
574
|
+
delete clean.stop
|
|
575
|
+
delete clean._status
|
|
576
|
+
delete clean._log
|
|
577
|
+
delete clean._progress
|
|
578
|
+
return clean
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function getRunOutputValue (result, output) {
|
|
582
|
+
if (!isRecordObject(result)) return result
|
|
583
|
+
const names = [output.name, sanitizeName(output.name)]
|
|
584
|
+
if (output.alias) names.push(output.alias)
|
|
585
|
+
for (const name of names) {
|
|
586
|
+
if (name && Object.prototype.hasOwnProperty.call(result, name)) return result[name]
|
|
587
|
+
}
|
|
588
|
+
if (Object.prototype.hasOwnProperty.call(output, 'value')) return output.value
|
|
589
|
+
return undefined
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function getRunOutputEntries (schema, result) {
|
|
593
|
+
const cleanResult = cleanRunResultObject(result)
|
|
594
|
+
const outputs = flattenOutputs(schema.outputs || [])
|
|
595
|
+
if (outputs.length) {
|
|
596
|
+
return outputs.map((output, index) => {
|
|
597
|
+
let value = getRunOutputValue(cleanResult, output)
|
|
598
|
+
if (typeof value === 'undefined' && outputs.length === 1 && !isRecordObject(cleanResult)) value = cleanResult
|
|
599
|
+
return {
|
|
600
|
+
output,
|
|
601
|
+
name: output.name || `output_${index + 1}`,
|
|
602
|
+
value
|
|
603
|
+
}
|
|
604
|
+
}).filter(entry => typeof entry.value !== 'undefined')
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (isRecordObject(cleanResult)) {
|
|
608
|
+
return Object.keys(cleanResult).map(key => ({
|
|
609
|
+
output: { name: key, type: typeof cleanResult[key] },
|
|
610
|
+
name: key,
|
|
611
|
+
value: cleanResult[key]
|
|
612
|
+
}))
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return [{
|
|
616
|
+
output: { name: 'result', type: typeof cleanResult },
|
|
617
|
+
name: 'result',
|
|
618
|
+
value: cleanResult
|
|
619
|
+
}]
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function getRunFileDescriptor (output, value) {
|
|
623
|
+
if (output.type !== 'file') {
|
|
624
|
+
return { value, filename: output.filename || output.name, mime: output.mime || output.contentType }
|
|
625
|
+
}
|
|
626
|
+
return normalizeFileOutputValue(output, value)
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function extensionFromMime (mime) {
|
|
630
|
+
if (!mime || typeof mime !== 'string') return null
|
|
631
|
+
const clean = mime.split(';')[0].trim().toLowerCase()
|
|
632
|
+
const map = {
|
|
633
|
+
'text/csv': '.csv',
|
|
634
|
+
'text/plain': '.txt',
|
|
635
|
+
'text/markdown': '.md',
|
|
636
|
+
'text/html': '.html',
|
|
637
|
+
'application/json': '.json',
|
|
638
|
+
'application/pdf': '.pdf',
|
|
639
|
+
'image/png': '.png',
|
|
640
|
+
'image/jpeg': '.jpg',
|
|
641
|
+
'image/svg+xml': '.svg'
|
|
642
|
+
}
|
|
643
|
+
return map[clean] || null
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function extensionForRunOutput (output, value, descriptor) {
|
|
647
|
+
if (descriptor && descriptor.filename && path.extname(descriptor.filename)) return path.extname(descriptor.filename)
|
|
648
|
+
if (descriptor && descriptor.mime) {
|
|
649
|
+
const mimeExt = extensionFromMime(descriptor.mime)
|
|
650
|
+
if (mimeExt) return mimeExt
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
switch ((output.type || '').toLowerCase()) {
|
|
654
|
+
case 'file':
|
|
655
|
+
return '.txt'
|
|
656
|
+
case 'markdown':
|
|
657
|
+
return '.md'
|
|
658
|
+
case 'html':
|
|
659
|
+
return '.html'
|
|
660
|
+
case 'code':
|
|
661
|
+
case 'text':
|
|
662
|
+
case 'string':
|
|
663
|
+
case 'chat':
|
|
664
|
+
return '.txt'
|
|
665
|
+
case 'image':
|
|
666
|
+
return '.png'
|
|
667
|
+
case 'pdf':
|
|
668
|
+
return '.pdf'
|
|
669
|
+
default:
|
|
670
|
+
return '.json'
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function getRunOutputFilename (entry) {
|
|
675
|
+
const descriptor = getRunFileDescriptor(entry.output, entry.value)
|
|
676
|
+
const base = descriptor.filename
|
|
677
|
+
? path.basename(String(descriptor.filename))
|
|
678
|
+
: sanitizeName(entry.name || 'output')
|
|
679
|
+
if (path.extname(base)) return base
|
|
680
|
+
return base + extensionForRunOutput(entry.output, descriptor.value, descriptor)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function formatRunOutputContent (entry, forStdout=false) {
|
|
684
|
+
const descriptor = getRunFileDescriptor(entry.output, entry.value)
|
|
685
|
+
const value = descriptor.value
|
|
686
|
+
const type = (entry.output.type || '').toLowerCase()
|
|
687
|
+
|
|
688
|
+
if (Buffer.isBuffer(value)) return { content: value, addNewline: false }
|
|
689
|
+
if (value instanceof Uint8Array) return { content: Buffer.from(value), addNewline: false }
|
|
690
|
+
|
|
691
|
+
if (type === 'file') {
|
|
692
|
+
if (isRecordObject(value) || Array.isArray(value)) {
|
|
693
|
+
return { content: JSON.stringify(value, null, 2), addNewline: true }
|
|
694
|
+
}
|
|
695
|
+
return { content: typeof value === 'undefined' || value === null ? '' : String(value), addNewline: false }
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (['text', 'string', 'code', 'markdown', 'html', 'chat'].includes(type)) {
|
|
699
|
+
if (isRecordObject(value) || Array.isArray(value)) {
|
|
700
|
+
return { content: JSON.stringify(value, null, 2), addNewline: true }
|
|
701
|
+
}
|
|
702
|
+
return { content: typeof value === 'undefined' || value === null ? '' : String(value), addNewline: true }
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (typeof value === 'string' && forStdout && !['table', 'object', 'array', 'json'].includes(type)) {
|
|
706
|
+
return { content: value, addNewline: true }
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return { content: JSON.stringify(value, null, 2), addNewline: true }
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function writeRunContent (target, formatted) {
|
|
713
|
+
let content = formatted.content
|
|
714
|
+
if (typeof content === 'string' && formatted.addNewline && !content.endsWith('\n')) content += '\n'
|
|
715
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
716
|
+
fs.writeFileSync(target, content)
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function writeRunStdout (formatted) {
|
|
720
|
+
let content = formatted.content
|
|
721
|
+
if (typeof content === 'string' && formatted.addNewline && !content.endsWith('\n')) content += '\n'
|
|
722
|
+
process.stdout.write(content)
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function getRunOutputsDirectory (outputs, cwd) {
|
|
726
|
+
if (!outputs) return null
|
|
727
|
+
const value = Array.isArray(outputs) ? outputs[0] : outputs
|
|
728
|
+
if (value === true || typeof value !== 'string') {
|
|
729
|
+
throw new Error('--outputs in --run mode expects a directory path')
|
|
730
|
+
}
|
|
731
|
+
return resolveOutputPath(cwd, value)
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function getRunOutputTarget (entry, argv, inputNames, cwd) {
|
|
735
|
+
const outputName = sanitizeName(entry.name)
|
|
736
|
+
const explicitKey = `output-${outputName}`
|
|
737
|
+
let value
|
|
738
|
+
if (Object.prototype.hasOwnProperty.call(argv, explicitKey)) {
|
|
739
|
+
value = argv[explicitKey]
|
|
740
|
+
} else if (!inputNames.has(outputName) && Object.prototype.hasOwnProperty.call(argv, outputName)) {
|
|
741
|
+
value = argv[outputName]
|
|
742
|
+
}
|
|
743
|
+
if (typeof value === 'undefined') return null
|
|
744
|
+
if (value === true || typeof value !== 'string') {
|
|
745
|
+
throw new Error(`Output target --${outputName} requires a file path`)
|
|
746
|
+
}
|
|
747
|
+
return resolveOutputPath(cwd, value)
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function emitRunOutputs (schema, result, argv, outputs, cwd) {
|
|
751
|
+
const entries = getRunOutputEntries(schema, result)
|
|
752
|
+
const inputNames = new Set((schema.inputs || []).map(input => sanitizeName(input.name)))
|
|
753
|
+
const outputDir = getRunOutputsDirectory(outputs, cwd)
|
|
754
|
+
const targets = new Map()
|
|
755
|
+
|
|
756
|
+
entries.forEach(entry => {
|
|
757
|
+
const target = getRunOutputTarget(entry, argv, inputNames, cwd)
|
|
758
|
+
if (target) targets.set(entry.name, target)
|
|
759
|
+
})
|
|
760
|
+
|
|
761
|
+
if (outputDir) {
|
|
762
|
+
fs.mkdirSync(outputDir, { recursive: true })
|
|
763
|
+
entries.forEach(entry => {
|
|
764
|
+
const target = targets.get(entry.name) || path.join(outputDir, getRunOutputFilename(entry))
|
|
765
|
+
writeRunContent(target, formatRunOutputContent(entry))
|
|
766
|
+
})
|
|
767
|
+
return
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (targets.size) {
|
|
771
|
+
entries.forEach(entry => {
|
|
772
|
+
const target = targets.get(entry.name)
|
|
773
|
+
if (target) writeRunContent(target, formatRunOutputContent(entry))
|
|
774
|
+
})
|
|
775
|
+
return
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (entries.length === 1) {
|
|
779
|
+
writeRunStdout(formatRunOutputContent(entries[0], true))
|
|
780
|
+
return
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const resultObject = {}
|
|
784
|
+
entries.forEach(entry => {
|
|
785
|
+
resultObject[entry.name] = entry.value
|
|
786
|
+
})
|
|
787
|
+
writeRunStdout({ content: JSON.stringify(resultObject, null, 2), addNewline: true })
|
|
788
|
+
}
|
|
789
|
+
|
|
262
790
|
let optionalEsbuild
|
|
263
791
|
function getOptionalEsbuild () {
|
|
264
792
|
if (optionalEsbuild) return optionalEsbuild
|
|
@@ -324,6 +852,64 @@ function buildBundledModelExposeCode (bundleGlobalName, modelName) {
|
|
|
324
852
|
`
|
|
325
853
|
}
|
|
326
854
|
|
|
855
|
+
|
|
856
|
+
function escapeRegExp (value) {
|
|
857
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function readPackageJsonForFile (filePath) {
|
|
861
|
+
try {
|
|
862
|
+
const packageRoot = findPackageRoot(filePath)
|
|
863
|
+
const packageJsonPath = path.join(packageRoot, 'package.json')
|
|
864
|
+
return {
|
|
865
|
+
packageRoot,
|
|
866
|
+
packageJson: JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
|
867
|
+
}
|
|
868
|
+
} catch (error) {
|
|
869
|
+
return null
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function createEmptyModulePlugin (specifiers) {
|
|
874
|
+
const list = Array.from(new Set(specifiers.filter(Boolean)))
|
|
875
|
+
if (!list.length) return null
|
|
876
|
+
const filter = new RegExp(`^(?:${list.map(escapeRegExp).join('|')})$`)
|
|
877
|
+
return {
|
|
878
|
+
name: 'jsee-empty-browser-modules',
|
|
879
|
+
setup (build) {
|
|
880
|
+
build.onResolve({ filter }, args => ({ path: args.path, namespace: 'jsee-empty' }))
|
|
881
|
+
build.onLoad({ filter: /.*/, namespace: 'jsee-empty' }, () => ({ contents: 'module.exports = {}', loader: 'js' }))
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function getBrowserFieldEsbuildOptions (modelPath) {
|
|
887
|
+
const packageInfo = readPackageJsonForFile(modelPath)
|
|
888
|
+
if (!packageInfo) return {}
|
|
889
|
+
|
|
890
|
+
const browser = packageInfo.packageJson.browser
|
|
891
|
+
if (!browser || typeof browser !== 'object' || Array.isArray(browser)) {
|
|
892
|
+
return { absWorkingDir: packageInfo.packageRoot }
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const alias = {}
|
|
896
|
+
const empty = []
|
|
897
|
+
Object.entries(browser).forEach(([key, value]) => {
|
|
898
|
+
if (!key || key.startsWith('.') || key.startsWith('/')) return
|
|
899
|
+
if (value === false) empty.push(key)
|
|
900
|
+
else if (typeof value === 'string' && value.length) alias[key] = value
|
|
901
|
+
})
|
|
902
|
+
|
|
903
|
+
const plugins = []
|
|
904
|
+
const emptyPlugin = createEmptyModulePlugin(empty)
|
|
905
|
+
if (emptyPlugin) plugins.push(emptyPlugin)
|
|
906
|
+
|
|
907
|
+
const options = { absWorkingDir: packageInfo.packageRoot }
|
|
908
|
+
if (Object.keys(alias).length) options.alias = alias
|
|
909
|
+
if (plugins.length) options.plugins = plugins
|
|
910
|
+
return options
|
|
911
|
+
}
|
|
912
|
+
|
|
327
913
|
async function bundleModelCode (model, modelPath, code) {
|
|
328
914
|
if (!shouldBundleModelCode(code)) return code
|
|
329
915
|
|
|
@@ -339,14 +925,14 @@ async function bundleModelCode (model, modelPath, code) {
|
|
|
339
925
|
throw new Error(`Bundling ${model.url || modelPath} requires a valid JavaScript model name or an explicit module export.`)
|
|
340
926
|
}
|
|
341
927
|
|
|
342
|
-
const options = {
|
|
928
|
+
const options = Object.assign({
|
|
343
929
|
bundle: true,
|
|
344
930
|
write: false,
|
|
345
931
|
platform: 'browser',
|
|
346
932
|
format: 'iife',
|
|
347
933
|
globalName: bundleGlobalName,
|
|
348
934
|
logLevel: 'silent'
|
|
349
|
-
}
|
|
935
|
+
}, getBrowserFieldEsbuildOptions(modelPath))
|
|
350
936
|
|
|
351
937
|
if (needsExportShim) {
|
|
352
938
|
options.stdin = {
|
|
@@ -704,10 +1290,18 @@ function template(schema, blocks) {
|
|
|
704
1290
|
table th { background-color: #f0f0f0; border: 1px solid #e0e0e0; }
|
|
705
1291
|
table td { border: 1px solid #e8e8e8; }
|
|
706
1292
|
@media screen and (max-width: 800px) { table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; -ms-overflow-style: -ms-autohiding-scrollbar; } }
|
|
707
|
-
.site-header { border-bottom: 1px solid #e8e8e8; min-height: 55.95px;
|
|
708
|
-
.site-
|
|
709
|
-
|
|
1293
|
+
.site-header { border-bottom: 1px solid #e8e8e8; min-height: 55.95px; position: relative; }
|
|
1294
|
+
.site-header .wrapper { min-height: 54px; display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
|
1295
|
+
.site-header .wrapper:after { content: none; }
|
|
1296
|
+
.site-title { font-size: 1.625rem; font-weight: 800; letter-spacing: -1px; line-height: 1.2; margin-bottom: 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
710
1297
|
.site-title, .site-title:visited { color: #424242; }
|
|
1298
|
+
.jsee-header-actions { display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex: 0 0 auto; font-size: 12px; line-height: 1; }
|
|
1299
|
+
.jsee-header-badge, .jsee-header-button { border: 1px solid #e0e0e0; border-radius: 4px; background: #f8f8f8; color: #555; padding: 6px 10px; font: inherit; line-height: 1; white-space: nowrap; }
|
|
1300
|
+
.jsee-header-badge { font-family: Menlo, Inconsolata, Consolas, Roboto Mono, Ubuntu Mono, Liberation Mono, Courier New, monospace; }
|
|
1301
|
+
.jsee-header-button { cursor: pointer; }
|
|
1302
|
+
.jsee-header-button:hover { background: #f0f0f0; color: #111; }
|
|
1303
|
+
.jsee-header-toggle { display: flex; align-items: center; gap: 4px; color: #666; cursor: pointer; white-space: nowrap; }
|
|
1304
|
+
@media screen and (max-width: 600px) { .site-header .wrapper { flex-wrap: wrap; gap: 8px; padding-top: 10px; padding-bottom: 10px; } .site-title { flex: 1 1 100%; white-space: normal; } .jsee-header-actions { width: 100%; justify-content: flex-start; } }
|
|
711
1305
|
.site-nav { position: absolute; top: 9px; right: 15px; background-color: #fdfdfd; border: 1px solid #e8e8e8; border-radius: 5px; text-align: right; }
|
|
712
1306
|
.site-nav .nav-trigger { display: none; }
|
|
713
1307
|
.site-nav .menu-icon { float: right; width: 36px; height: 26px; line-height: 0; padding-top: 10px; text-align: center; }
|
|
@@ -767,7 +1361,6 @@ function template(schema, blocks) {
|
|
|
767
1361
|
@media screen and (min-width: 800px) { .one-half { width: calc(50% - (30px / 2)); } }
|
|
768
1362
|
/** Jsee elements */
|
|
769
1363
|
.app-container { background-color: #F0F1F4; border-bottom: 1px solid #e8e8e8; padding-bottom: 55px }
|
|
770
|
-
#save-html-btn:hover { background-color: #f0f0f0 !important; }
|
|
771
1364
|
.schema-description { background-color: #f8f8fa; padding: 20px; margin-top: 20px; border-radius: 10px; border: 1px solid #e8e8e8; }
|
|
772
1365
|
.schema-description h2, .schema-description h3, .schema-description h4 { margin-top: 10px; }
|
|
773
1366
|
/** Logos */
|
|
@@ -782,10 +1375,10 @@ function template(schema, blocks) {
|
|
|
782
1375
|
</head>
|
|
783
1376
|
<body>
|
|
784
1377
|
${blocks.hiddenElementHtml}
|
|
785
|
-
${blocks.serveBarHtml}
|
|
786
1378
|
<header class="site-header">
|
|
787
1379
|
<div class="wrapper">
|
|
788
1380
|
<span class="site-title">${title}</span>
|
|
1381
|
+
${blocks.headerRightHtml}
|
|
789
1382
|
</div>
|
|
790
1383
|
</header>
|
|
791
1384
|
<div class="page-content app-container">
|
|
@@ -836,9 +1429,39 @@ function template(schema, blocks) {
|
|
|
836
1429
|
env = new JSEE({ container: container, schema: currentSchema })
|
|
837
1430
|
})
|
|
838
1431
|
}
|
|
839
|
-
var
|
|
840
|
-
if (
|
|
841
|
-
|
|
1432
|
+
var downloadBtn = document.getElementById('download-html-btn')
|
|
1433
|
+
if (downloadBtn) {
|
|
1434
|
+
var downloadTitle = ${JSON.stringify(title)}
|
|
1435
|
+
function formatHtmlSize(bytes) {
|
|
1436
|
+
if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
|
|
1437
|
+
if (bytes >= 1024) return Math.round(bytes / 1024) + ' KB'
|
|
1438
|
+
return bytes + ' B'
|
|
1439
|
+
}
|
|
1440
|
+
function getDownloadHtml() {
|
|
1441
|
+
return '<!DOCTYPE html>\\n' + document.documentElement.outerHTML
|
|
1442
|
+
}
|
|
1443
|
+
function getDownloadFilename() {
|
|
1444
|
+
var base = (downloadTitle || 'jsee').toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '')
|
|
1445
|
+
return (base || 'jsee') + '.html'
|
|
1446
|
+
}
|
|
1447
|
+
function updateDownloadLabel() {
|
|
1448
|
+
var html = getDownloadHtml()
|
|
1449
|
+
var size = new Blob([html], { type: 'text/html' }).size
|
|
1450
|
+
downloadBtn.textContent = 'Download HTML - ' + formatHtmlSize(size)
|
|
1451
|
+
}
|
|
1452
|
+
downloadBtn.addEventListener('click', function () {
|
|
1453
|
+
var html = getDownloadHtml()
|
|
1454
|
+
var blob = new Blob([html], { type: 'text/html' })
|
|
1455
|
+
var url = URL.createObjectURL(blob)
|
|
1456
|
+
var a = document.createElement('a')
|
|
1457
|
+
a.href = url
|
|
1458
|
+
a.download = getDownloadFilename()
|
|
1459
|
+
document.body.appendChild(a)
|
|
1460
|
+
a.click()
|
|
1461
|
+
a.remove()
|
|
1462
|
+
setTimeout(function () { URL.revokeObjectURL(url) }, 0)
|
|
1463
|
+
})
|
|
1464
|
+
setTimeout(updateDownloadLabel, 0)
|
|
842
1465
|
}
|
|
843
1466
|
</script>
|
|
844
1467
|
</body>
|
|
@@ -863,6 +1486,7 @@ async function gen (pargv, returnHtml=false) {
|
|
|
863
1486
|
execute: 'e',
|
|
864
1487
|
cdn: 'c',
|
|
865
1488
|
runtime: 'r',
|
|
1489
|
+
serve: 's'
|
|
866
1490
|
}
|
|
867
1491
|
const argvDefault = {
|
|
868
1492
|
execute: 'auto', // execute the model code on the server (auto = server-side when serving local .js models)
|
|
@@ -872,12 +1496,15 @@ async function gen (pargv, returnHtml=false) {
|
|
|
872
1496
|
version: 'latest', // default version of JSEE runtime to use
|
|
873
1497
|
verbose: false, // verbose mode
|
|
874
1498
|
cdn: false,
|
|
875
|
-
runtime: 'auto'
|
|
1499
|
+
runtime: 'auto',
|
|
1500
|
+
serve: false,
|
|
1501
|
+
run: false
|
|
876
1502
|
}
|
|
1503
|
+
const booleanArgs = ['help', 'h', 'html', 'client', 'serve', 'run']
|
|
877
1504
|
let argv = minimist(pargv, {
|
|
878
1505
|
alias: argvAlias,
|
|
879
1506
|
default: argvDefault,
|
|
880
|
-
boolean:
|
|
1507
|
+
boolean: booleanArgs,
|
|
881
1508
|
})
|
|
882
1509
|
|
|
883
1510
|
// ── jsee init <template> ──────────────────────────────────────────
|
|
@@ -1015,7 +1642,7 @@ npx @jseeio/jsee schema.json
|
|
|
1015
1642
|
|
|
1016
1643
|
if (argv.help || argv.h) {
|
|
1017
1644
|
console.log(`
|
|
1018
|
-
Usage: jsee [schema.json] [data...] [options]
|
|
1645
|
+
Usage: jsee [schema.json|package] [data...] [options]
|
|
1019
1646
|
jsee init [template] [--html]
|
|
1020
1647
|
|
|
1021
1648
|
Commands:
|
|
@@ -1030,6 +1657,8 @@ Options:
|
|
|
1030
1657
|
-v, --version <version> JSEE runtime version (default: latest)
|
|
1031
1658
|
-b, --bundle Bundle runtime + dependencies into output
|
|
1032
1659
|
-f, --fetch Alias for --bundle
|
|
1660
|
+
-s, --serve Serve explicitly (default when no output is provided)
|
|
1661
|
+
--run Execute the model once and write pipeable outputs
|
|
1033
1662
|
-e, --execute Execute model server-side (auto-enabled when serving local .js models)
|
|
1034
1663
|
--client Force client-side execution (disable auto server-side)
|
|
1035
1664
|
-c, --cdn <url|bool> Rewrite model URLs for CDN deployment
|
|
@@ -1047,11 +1676,14 @@ Examples:
|
|
|
1047
1676
|
jsee init chat Scaffold chat project
|
|
1048
1677
|
jsee init --html Generate single index.html
|
|
1049
1678
|
jsee schema.json Start dev server with schema
|
|
1679
|
+
jsee @scope/app --serve Start dev server from a JSEE app package
|
|
1050
1680
|
jsee schema.json 42 hello Pass positional data to first two inputs
|
|
1051
1681
|
jsee schema.json --a=100 --b=200 Pass named data inputs
|
|
1052
1682
|
jsee schema.json data.csv Pass a file path as input
|
|
1053
1683
|
jsee schema.json -o app.html Generate static HTML file
|
|
1054
1684
|
jsee schema.json -o app.html --bundle Generate self-contained HTML with bundled runtime
|
|
1685
|
+
jsee @statsim/gen --run --dataset Moons --format CSV --nSamples 500
|
|
1686
|
+
jsee @statsim/gen --run --dataset Moons --file moons.csv
|
|
1055
1687
|
jsee -p 8080 Start dev server on port 8080
|
|
1056
1688
|
jsee report.pdf Serve a PDF file (auto-detected viewer)
|
|
1057
1689
|
jsee data/ Serve a folder (file browser with preview)
|
|
@@ -1061,14 +1693,18 @@ Documentation: https://jsee.org
|
|
|
1061
1693
|
return
|
|
1062
1694
|
}
|
|
1063
1695
|
|
|
1064
|
-
// Check if first positional arg is a file or
|
|
1696
|
+
// Check if first positional arg is a file/directory or JSEE app package.
|
|
1065
1697
|
let identityResult = null
|
|
1698
|
+
let packageInput = null
|
|
1066
1699
|
if (!imported && argv._.length > 0) {
|
|
1067
1700
|
identityResult = generateIdentitySchema(argv._[0], process.cwd())
|
|
1701
|
+
if (!identityResult) {
|
|
1702
|
+
packageInput = resolveJseePackageInput(argv._[0], process.cwd())
|
|
1703
|
+
}
|
|
1068
1704
|
}
|
|
1069
1705
|
|
|
1070
|
-
// Set argv.inputs to the first positional argument if it looks like a schema/script
|
|
1071
|
-
if (!identityResult && !imported && argv._.length > 0 && argv.inputs === argvDefault.inputs) {
|
|
1706
|
+
// Set argv.inputs to the first positional argument if it looks like a schema/script/package.
|
|
1707
|
+
if (!identityResult && !packageInput && !imported && argv._.length > 0 && argv.inputs === argvDefault.inputs) {
|
|
1072
1708
|
argv.inputs = argv._[0]
|
|
1073
1709
|
}
|
|
1074
1710
|
|
|
@@ -1087,8 +1723,22 @@ Documentation: https://jsee.org
|
|
|
1087
1723
|
|
|
1088
1724
|
let cwd = process.cwd()
|
|
1089
1725
|
let inputs = argv.inputs
|
|
1090
|
-
let outputs = argv.outputs
|
|
1091
|
-
let description = argv.description
|
|
1726
|
+
let outputs = argv.serve ? false : argv.outputs
|
|
1727
|
+
let description = argv.description || ''
|
|
1728
|
+
|
|
1729
|
+
if (packageInput || (!identityResult && typeof inputs === 'string')) {
|
|
1730
|
+
const resolvedPackageInput = packageInput || resolveJseePackageInput(inputs, cwd)
|
|
1731
|
+
if (resolvedPackageInput) {
|
|
1732
|
+
packageInput = resolvedPackageInput
|
|
1733
|
+
cwd = packageInput.packageRoot
|
|
1734
|
+
inputs = [packageInput.schemaPath]
|
|
1735
|
+
if (!description && packageInput.descriptionPath) {
|
|
1736
|
+
description = path.relative(cwd, packageInput.descriptionPath)
|
|
1737
|
+
}
|
|
1738
|
+
} else if (looksLikeMissingPackageInput(inputs, cwd)) {
|
|
1739
|
+
throw new Error(`Cannot resolve JSEE app package ${inputs}. Install it in this project or let npm provide both packages:\n ${getPackageInputInstallHint(inputs)}`)
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1092
1742
|
let schema
|
|
1093
1743
|
let schemaPath
|
|
1094
1744
|
let descriptionTxt = ''
|
|
@@ -1099,13 +1749,13 @@ Documentation: https://jsee.org
|
|
|
1099
1749
|
// Determine the inputs and outputs
|
|
1100
1750
|
// if inputs is a string with js file names, split it into an array
|
|
1101
1751
|
if (typeof inputs === 'string') {
|
|
1102
|
-
if (inputs.includes('.js')) {
|
|
1752
|
+
if (inputs.includes('.js') || inputs.includes('.json') || inputs.includes(',')) {
|
|
1103
1753
|
inputs = inputs.split(',')
|
|
1104
1754
|
}
|
|
1105
1755
|
}
|
|
1106
1756
|
|
|
1107
1757
|
// if outputs is a string with js file names, split it into an array
|
|
1108
|
-
if (typeof outputs === 'string') {
|
|
1758
|
+
if (typeof outputs === 'string' && !argv.run) {
|
|
1109
1759
|
outputs = outputs.split(',')
|
|
1110
1760
|
}
|
|
1111
1761
|
|
|
@@ -1150,8 +1800,11 @@ Documentation: https://jsee.org
|
|
|
1150
1800
|
schema.inputs.forEach((inp, inp_index) => {
|
|
1151
1801
|
if (inp.name) {
|
|
1152
1802
|
const inputName = sanitizeName(inp.name)
|
|
1153
|
-
|
|
1154
|
-
|
|
1803
|
+
const inputAliases = []
|
|
1804
|
+
if (inp.alias) inputAliases.push(...toArray(inp.alias))
|
|
1805
|
+
if (inp.name !== inputName) inputAliases.push(inp.name)
|
|
1806
|
+
if (inputAliases.length) {
|
|
1807
|
+
argvAlias[inputName] = inputAliases
|
|
1155
1808
|
}
|
|
1156
1809
|
// Use positional arguments as schema input defaults
|
|
1157
1810
|
if (imported && argv._.length > inp_index) {
|
|
@@ -1174,6 +1827,7 @@ Documentation: https://jsee.org
|
|
|
1174
1827
|
argv = minimist(pargv, {
|
|
1175
1828
|
alias: argvAlias,
|
|
1176
1829
|
default: argvDefault,
|
|
1830
|
+
boolean: booleanArgs
|
|
1177
1831
|
})
|
|
1178
1832
|
|
|
1179
1833
|
// Now deactivate the inputs present in argv
|
|
@@ -1204,6 +1858,17 @@ Documentation: https://jsee.org
|
|
|
1204
1858
|
schema.model = [schema.model]
|
|
1205
1859
|
}
|
|
1206
1860
|
|
|
1861
|
+
if (argv.run) {
|
|
1862
|
+
const data = Object.assign(
|
|
1863
|
+
{},
|
|
1864
|
+
getSchemaInputDefaults(schema),
|
|
1865
|
+
getDataFromArgv(schema, argv, true)
|
|
1866
|
+
)
|
|
1867
|
+
const result = await runSchemaOnce(schema, data, schemaPath, cwd, log)
|
|
1868
|
+
emitRunOutputs(schema, result, argv, outputs, process.cwd())
|
|
1869
|
+
return result
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1207
1872
|
// Resolve server-side execution mode
|
|
1208
1873
|
const hasOutputs = Array.isArray(outputs) ? outputs.length > 0 : Boolean(outputs)
|
|
1209
1874
|
let shouldExecute = argv.execute
|
|
@@ -1230,18 +1895,7 @@ Documentation: https://jsee.org
|
|
|
1230
1895
|
if (shouldExecute) {
|
|
1231
1896
|
await Promise.all(schema.model.map(async m => {
|
|
1232
1897
|
log('Preparing a model to run on the server side:', m.name, m.url)
|
|
1233
|
-
|
|
1234
|
-
let target = require(modelPath)
|
|
1235
|
-
// Handle browser-style model files (no module.exports) — eval the source
|
|
1236
|
-
// to extract the named function, similar to how the worker does it
|
|
1237
|
-
if (typeof target !== 'function' && (!target || Object.keys(target).length === 0)) {
|
|
1238
|
-
const src = fs.readFileSync(modelPath, 'utf-8')
|
|
1239
|
-
const fn = new Function(src + `\nreturn typeof ${m.name} === 'function' ? ${m.name} : undefined`)()
|
|
1240
|
-
if (typeof fn === 'function') {
|
|
1241
|
-
target = fn
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
modelFuncs[m.name] = await getModelFuncJS(m, target, {log})
|
|
1898
|
+
modelFuncs[m.name] = await loadCliModelFunction(m, schemaPath, cwd, log)
|
|
1245
1899
|
m.type = 'post'
|
|
1246
1900
|
m.url = `/${m.name}`
|
|
1247
1901
|
m.worker = false
|
|
@@ -1285,7 +1939,8 @@ Documentation: https://jsee.org
|
|
|
1285
1939
|
|
|
1286
1940
|
// Generate description block
|
|
1287
1941
|
if (description) {
|
|
1288
|
-
const
|
|
1942
|
+
const descriptionPath = path.isAbsolute(description) ? description : path.join(cwd, description)
|
|
1943
|
+
const descriptionMd = fs.readFileSync(descriptionPath, 'utf8')
|
|
1289
1944
|
descriptionHtml = getMarkdownConverter().render(descriptionMd)
|
|
1290
1945
|
|
|
1291
1946
|
if (descriptionMd.includes('---')) {
|
|
@@ -1455,20 +2110,17 @@ Documentation: https://jsee.org
|
|
|
1455
2110
|
|
|
1456
2111
|
}
|
|
1457
2112
|
|
|
1458
|
-
// Build
|
|
1459
|
-
let
|
|
2113
|
+
// Build right-side header status/action. Serve mode shows only runtime status; bundled files can download themselves.
|
|
2114
|
+
let headerRightHtml = ''
|
|
1460
2115
|
let schemaScript = ''
|
|
1461
2116
|
const isServing = !hasOutputs
|
|
1462
2117
|
if (isServing) {
|
|
1463
2118
|
const toggleHtml = shouldExecute
|
|
1464
|
-
? '<label
|
|
2119
|
+
? '<label class="jsee-header-toggle"><input type="checkbox" id="exec-toggle">Browser</label>'
|
|
1465
2120
|
: ''
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
${toggleHtml}
|
|
1470
|
-
<button id="save-html-btn" style="background:none;border:1px solid #ddd;border-radius:3px;padding:3px 10px;font-size:12px;color:#555;cursor:pointer" title="Save as self-contained HTML file">Save HTML</button>
|
|
1471
|
-
</div>`
|
|
2121
|
+
headerRightHtml = `<div class="jsee-header-actions">${toggleHtml}<span class="jsee-header-badge">localhost:${argv.port}</span></div>`
|
|
2122
|
+
} else if (shouldBundle) {
|
|
2123
|
+
headerRightHtml = '<div class="jsee-header-actions"><button id="download-html-btn" class="jsee-header-button" type="button" title="Download this standalone HTML file">Download HTML</button></div>'
|
|
1472
2124
|
}
|
|
1473
2125
|
if (shouldExecute) {
|
|
1474
2126
|
const clientSchema = JSON.parse(JSON.stringify(schema))
|
|
@@ -1486,7 +2138,7 @@ Documentation: https://jsee.org
|
|
|
1486
2138
|
hiddenElementHtml: hiddenElementHtml,
|
|
1487
2139
|
socialHtml: pad(socialHtml, 2, 1),
|
|
1488
2140
|
orgHtml: pad(orgHtml, 2, 1),
|
|
1489
|
-
|
|
2141
|
+
headerRightHtml: headerRightHtml,
|
|
1490
2142
|
schemaScript: schemaScript,
|
|
1491
2143
|
})
|
|
1492
2144
|
|
|
@@ -1499,11 +2151,11 @@ Documentation: https://jsee.org
|
|
|
1499
2151
|
if (o === 'stdout') {
|
|
1500
2152
|
log(html)
|
|
1501
2153
|
} else if (o.includes('.html')) {
|
|
1502
|
-
|
|
2154
|
+
writeOutputFile(cwd, o, html)
|
|
1503
2155
|
} else if (o.includes('.json')) {
|
|
1504
|
-
|
|
2156
|
+
writeOutputFile(cwd, o, JSON.stringify(schema, null, 2))
|
|
1505
2157
|
} else if (o.includes('.md')) {
|
|
1506
|
-
|
|
2158
|
+
writeOutputFile(cwd, o, genMarkdownFromSchema(schema))
|
|
1507
2159
|
} else {
|
|
1508
2160
|
console.error('Invalid output file:', o)
|
|
1509
2161
|
}
|
|
@@ -1613,6 +2265,16 @@ module.exports.resolveLocalImportFile = resolveLocalImportFile
|
|
|
1613
2265
|
module.exports.resolveFetchImport = resolveFetchImport
|
|
1614
2266
|
module.exports.resolveRuntimeMode = resolveRuntimeMode
|
|
1615
2267
|
module.exports.resolveOutputPath = resolveOutputPath
|
|
2268
|
+
module.exports.resolveJseePackageInput = resolveJseePackageInput
|
|
2269
|
+
module.exports.readJseePackageInput = readJseePackageInput
|
|
2270
|
+
module.exports.findPackageRoot = findPackageRoot
|
|
2271
|
+
module.exports.runPackage = runPackage
|
|
2272
|
+
module.exports.looksLikeMissingPackageInput = looksLikeMissingPackageInput
|
|
2273
|
+
module.exports.getPackageInputInstallHint = getPackageInputInstallHint
|
|
2274
|
+
module.exports.isPackageSpecifier = isPackageSpecifier
|
|
2275
|
+
module.exports.writeOutputFile = writeOutputFile
|
|
1616
2276
|
module.exports.needsFullBundle = needsFullBundle
|
|
1617
2277
|
module.exports.shouldBundleModelCode = shouldBundleModelCode
|
|
1618
2278
|
module.exports.bundleModelCode = bundleModelCode
|
|
2279
|
+
module.exports.runSchemaOnce = runSchemaOnce
|
|
2280
|
+
module.exports.emitRunOutputs = emitRunOutputs
|