@jseeio/jsee 0.8.2 → 0.8.7
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 +8 -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 +651 -47
- 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
|
|
@@ -704,10 +1232,18 @@ function template(schema, blocks) {
|
|
|
704
1232
|
table th { background-color: #f0f0f0; border: 1px solid #e0e0e0; }
|
|
705
1233
|
table td { border: 1px solid #e8e8e8; }
|
|
706
1234
|
@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
|
-
|
|
1235
|
+
.site-header { border-bottom: 1px solid #e8e8e8; min-height: 55.95px; position: relative; }
|
|
1236
|
+
.site-header .wrapper { min-height: 54px; display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
|
1237
|
+
.site-header .wrapper:after { content: none; }
|
|
1238
|
+
.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
1239
|
.site-title, .site-title:visited { color: #424242; }
|
|
1240
|
+
.jsee-header-actions { display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex: 0 0 auto; font-size: 12px; line-height: 1; }
|
|
1241
|
+
.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; }
|
|
1242
|
+
.jsee-header-badge { font-family: Menlo, Inconsolata, Consolas, Roboto Mono, Ubuntu Mono, Liberation Mono, Courier New, monospace; }
|
|
1243
|
+
.jsee-header-button { cursor: pointer; }
|
|
1244
|
+
.jsee-header-button:hover { background: #f0f0f0; color: #111; }
|
|
1245
|
+
.jsee-header-toggle { display: flex; align-items: center; gap: 4px; color: #666; cursor: pointer; white-space: nowrap; }
|
|
1246
|
+
@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
1247
|
.site-nav { position: absolute; top: 9px; right: 15px; background-color: #fdfdfd; border: 1px solid #e8e8e8; border-radius: 5px; text-align: right; }
|
|
712
1248
|
.site-nav .nav-trigger { display: none; }
|
|
713
1249
|
.site-nav .menu-icon { float: right; width: 36px; height: 26px; line-height: 0; padding-top: 10px; text-align: center; }
|
|
@@ -767,7 +1303,6 @@ function template(schema, blocks) {
|
|
|
767
1303
|
@media screen and (min-width: 800px) { .one-half { width: calc(50% - (30px / 2)); } }
|
|
768
1304
|
/** Jsee elements */
|
|
769
1305
|
.app-container { background-color: #F0F1F4; border-bottom: 1px solid #e8e8e8; padding-bottom: 55px }
|
|
770
|
-
#save-html-btn:hover { background-color: #f0f0f0 !important; }
|
|
771
1306
|
.schema-description { background-color: #f8f8fa; padding: 20px; margin-top: 20px; border-radius: 10px; border: 1px solid #e8e8e8; }
|
|
772
1307
|
.schema-description h2, .schema-description h3, .schema-description h4 { margin-top: 10px; }
|
|
773
1308
|
/** Logos */
|
|
@@ -782,10 +1317,10 @@ function template(schema, blocks) {
|
|
|
782
1317
|
</head>
|
|
783
1318
|
<body>
|
|
784
1319
|
${blocks.hiddenElementHtml}
|
|
785
|
-
${blocks.serveBarHtml}
|
|
786
1320
|
<header class="site-header">
|
|
787
1321
|
<div class="wrapper">
|
|
788
1322
|
<span class="site-title">${title}</span>
|
|
1323
|
+
${blocks.headerRightHtml}
|
|
789
1324
|
</div>
|
|
790
1325
|
</header>
|
|
791
1326
|
<div class="page-content app-container">
|
|
@@ -836,9 +1371,39 @@ function template(schema, blocks) {
|
|
|
836
1371
|
env = new JSEE({ container: container, schema: currentSchema })
|
|
837
1372
|
})
|
|
838
1373
|
}
|
|
839
|
-
var
|
|
840
|
-
if (
|
|
841
|
-
|
|
1374
|
+
var downloadBtn = document.getElementById('download-html-btn')
|
|
1375
|
+
if (downloadBtn) {
|
|
1376
|
+
var downloadTitle = ${JSON.stringify(title)}
|
|
1377
|
+
function formatHtmlSize(bytes) {
|
|
1378
|
+
if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
|
|
1379
|
+
if (bytes >= 1024) return Math.round(bytes / 1024) + ' KB'
|
|
1380
|
+
return bytes + ' B'
|
|
1381
|
+
}
|
|
1382
|
+
function getDownloadHtml() {
|
|
1383
|
+
return '<!DOCTYPE html>\\n' + document.documentElement.outerHTML
|
|
1384
|
+
}
|
|
1385
|
+
function getDownloadFilename() {
|
|
1386
|
+
var base = (downloadTitle || 'jsee').toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '')
|
|
1387
|
+
return (base || 'jsee') + '.html'
|
|
1388
|
+
}
|
|
1389
|
+
function updateDownloadLabel() {
|
|
1390
|
+
var html = getDownloadHtml()
|
|
1391
|
+
var size = new Blob([html], { type: 'text/html' }).size
|
|
1392
|
+
downloadBtn.textContent = 'Download HTML - ' + formatHtmlSize(size)
|
|
1393
|
+
}
|
|
1394
|
+
downloadBtn.addEventListener('click', function () {
|
|
1395
|
+
var html = getDownloadHtml()
|
|
1396
|
+
var blob = new Blob([html], { type: 'text/html' })
|
|
1397
|
+
var url = URL.createObjectURL(blob)
|
|
1398
|
+
var a = document.createElement('a')
|
|
1399
|
+
a.href = url
|
|
1400
|
+
a.download = getDownloadFilename()
|
|
1401
|
+
document.body.appendChild(a)
|
|
1402
|
+
a.click()
|
|
1403
|
+
a.remove()
|
|
1404
|
+
setTimeout(function () { URL.revokeObjectURL(url) }, 0)
|
|
1405
|
+
})
|
|
1406
|
+
setTimeout(updateDownloadLabel, 0)
|
|
842
1407
|
}
|
|
843
1408
|
</script>
|
|
844
1409
|
</body>
|
|
@@ -863,6 +1428,7 @@ async function gen (pargv, returnHtml=false) {
|
|
|
863
1428
|
execute: 'e',
|
|
864
1429
|
cdn: 'c',
|
|
865
1430
|
runtime: 'r',
|
|
1431
|
+
serve: 's'
|
|
866
1432
|
}
|
|
867
1433
|
const argvDefault = {
|
|
868
1434
|
execute: 'auto', // execute the model code on the server (auto = server-side when serving local .js models)
|
|
@@ -872,12 +1438,15 @@ async function gen (pargv, returnHtml=false) {
|
|
|
872
1438
|
version: 'latest', // default version of JSEE runtime to use
|
|
873
1439
|
verbose: false, // verbose mode
|
|
874
1440
|
cdn: false,
|
|
875
|
-
runtime: 'auto'
|
|
1441
|
+
runtime: 'auto',
|
|
1442
|
+
serve: false,
|
|
1443
|
+
run: false
|
|
876
1444
|
}
|
|
1445
|
+
const booleanArgs = ['help', 'h', 'html', 'client', 'serve', 'run']
|
|
877
1446
|
let argv = minimist(pargv, {
|
|
878
1447
|
alias: argvAlias,
|
|
879
1448
|
default: argvDefault,
|
|
880
|
-
boolean:
|
|
1449
|
+
boolean: booleanArgs,
|
|
881
1450
|
})
|
|
882
1451
|
|
|
883
1452
|
// ── jsee init <template> ──────────────────────────────────────────
|
|
@@ -1015,7 +1584,7 @@ npx @jseeio/jsee schema.json
|
|
|
1015
1584
|
|
|
1016
1585
|
if (argv.help || argv.h) {
|
|
1017
1586
|
console.log(`
|
|
1018
|
-
Usage: jsee [schema.json] [data...] [options]
|
|
1587
|
+
Usage: jsee [schema.json|package] [data...] [options]
|
|
1019
1588
|
jsee init [template] [--html]
|
|
1020
1589
|
|
|
1021
1590
|
Commands:
|
|
@@ -1030,6 +1599,8 @@ Options:
|
|
|
1030
1599
|
-v, --version <version> JSEE runtime version (default: latest)
|
|
1031
1600
|
-b, --bundle Bundle runtime + dependencies into output
|
|
1032
1601
|
-f, --fetch Alias for --bundle
|
|
1602
|
+
-s, --serve Serve explicitly (default when no output is provided)
|
|
1603
|
+
--run Execute the model once and write pipeable outputs
|
|
1033
1604
|
-e, --execute Execute model server-side (auto-enabled when serving local .js models)
|
|
1034
1605
|
--client Force client-side execution (disable auto server-side)
|
|
1035
1606
|
-c, --cdn <url|bool> Rewrite model URLs for CDN deployment
|
|
@@ -1047,11 +1618,14 @@ Examples:
|
|
|
1047
1618
|
jsee init chat Scaffold chat project
|
|
1048
1619
|
jsee init --html Generate single index.html
|
|
1049
1620
|
jsee schema.json Start dev server with schema
|
|
1621
|
+
jsee @scope/app --serve Start dev server from a JSEE app package
|
|
1050
1622
|
jsee schema.json 42 hello Pass positional data to first two inputs
|
|
1051
1623
|
jsee schema.json --a=100 --b=200 Pass named data inputs
|
|
1052
1624
|
jsee schema.json data.csv Pass a file path as input
|
|
1053
1625
|
jsee schema.json -o app.html Generate static HTML file
|
|
1054
1626
|
jsee schema.json -o app.html --bundle Generate self-contained HTML with bundled runtime
|
|
1627
|
+
jsee @statsim/gen --run --dataset Moons --format CSV --nSamples 500
|
|
1628
|
+
jsee @statsim/gen --run --dataset Moons --file moons.csv
|
|
1055
1629
|
jsee -p 8080 Start dev server on port 8080
|
|
1056
1630
|
jsee report.pdf Serve a PDF file (auto-detected viewer)
|
|
1057
1631
|
jsee data/ Serve a folder (file browser with preview)
|
|
@@ -1061,14 +1635,18 @@ Documentation: https://jsee.org
|
|
|
1061
1635
|
return
|
|
1062
1636
|
}
|
|
1063
1637
|
|
|
1064
|
-
// Check if first positional arg is a file or
|
|
1638
|
+
// Check if first positional arg is a file/directory or JSEE app package.
|
|
1065
1639
|
let identityResult = null
|
|
1640
|
+
let packageInput = null
|
|
1066
1641
|
if (!imported && argv._.length > 0) {
|
|
1067
1642
|
identityResult = generateIdentitySchema(argv._[0], process.cwd())
|
|
1643
|
+
if (!identityResult) {
|
|
1644
|
+
packageInput = resolveJseePackageInput(argv._[0], process.cwd())
|
|
1645
|
+
}
|
|
1068
1646
|
}
|
|
1069
1647
|
|
|
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) {
|
|
1648
|
+
// Set argv.inputs to the first positional argument if it looks like a schema/script/package.
|
|
1649
|
+
if (!identityResult && !packageInput && !imported && argv._.length > 0 && argv.inputs === argvDefault.inputs) {
|
|
1072
1650
|
argv.inputs = argv._[0]
|
|
1073
1651
|
}
|
|
1074
1652
|
|
|
@@ -1087,8 +1665,22 @@ Documentation: https://jsee.org
|
|
|
1087
1665
|
|
|
1088
1666
|
let cwd = process.cwd()
|
|
1089
1667
|
let inputs = argv.inputs
|
|
1090
|
-
let outputs = argv.outputs
|
|
1091
|
-
let description = argv.description
|
|
1668
|
+
let outputs = argv.serve ? false : argv.outputs
|
|
1669
|
+
let description = argv.description || ''
|
|
1670
|
+
|
|
1671
|
+
if (packageInput || (!identityResult && typeof inputs === 'string')) {
|
|
1672
|
+
const resolvedPackageInput = packageInput || resolveJseePackageInput(inputs, cwd)
|
|
1673
|
+
if (resolvedPackageInput) {
|
|
1674
|
+
packageInput = resolvedPackageInput
|
|
1675
|
+
cwd = packageInput.packageRoot
|
|
1676
|
+
inputs = [packageInput.schemaPath]
|
|
1677
|
+
if (!description && packageInput.descriptionPath) {
|
|
1678
|
+
description = path.relative(cwd, packageInput.descriptionPath)
|
|
1679
|
+
}
|
|
1680
|
+
} else if (looksLikeMissingPackageInput(inputs, cwd)) {
|
|
1681
|
+
throw new Error(`Cannot resolve JSEE app package ${inputs}. Install it in this project or let npm provide both packages:\n ${getPackageInputInstallHint(inputs)}`)
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1092
1684
|
let schema
|
|
1093
1685
|
let schemaPath
|
|
1094
1686
|
let descriptionTxt = ''
|
|
@@ -1099,13 +1691,13 @@ Documentation: https://jsee.org
|
|
|
1099
1691
|
// Determine the inputs and outputs
|
|
1100
1692
|
// if inputs is a string with js file names, split it into an array
|
|
1101
1693
|
if (typeof inputs === 'string') {
|
|
1102
|
-
if (inputs.includes('.js')) {
|
|
1694
|
+
if (inputs.includes('.js') || inputs.includes('.json') || inputs.includes(',')) {
|
|
1103
1695
|
inputs = inputs.split(',')
|
|
1104
1696
|
}
|
|
1105
1697
|
}
|
|
1106
1698
|
|
|
1107
1699
|
// if outputs is a string with js file names, split it into an array
|
|
1108
|
-
if (typeof outputs === 'string') {
|
|
1700
|
+
if (typeof outputs === 'string' && !argv.run) {
|
|
1109
1701
|
outputs = outputs.split(',')
|
|
1110
1702
|
}
|
|
1111
1703
|
|
|
@@ -1150,8 +1742,11 @@ Documentation: https://jsee.org
|
|
|
1150
1742
|
schema.inputs.forEach((inp, inp_index) => {
|
|
1151
1743
|
if (inp.name) {
|
|
1152
1744
|
const inputName = sanitizeName(inp.name)
|
|
1153
|
-
|
|
1154
|
-
|
|
1745
|
+
const inputAliases = []
|
|
1746
|
+
if (inp.alias) inputAliases.push(...toArray(inp.alias))
|
|
1747
|
+
if (inp.name !== inputName) inputAliases.push(inp.name)
|
|
1748
|
+
if (inputAliases.length) {
|
|
1749
|
+
argvAlias[inputName] = inputAliases
|
|
1155
1750
|
}
|
|
1156
1751
|
// Use positional arguments as schema input defaults
|
|
1157
1752
|
if (imported && argv._.length > inp_index) {
|
|
@@ -1174,6 +1769,7 @@ Documentation: https://jsee.org
|
|
|
1174
1769
|
argv = minimist(pargv, {
|
|
1175
1770
|
alias: argvAlias,
|
|
1176
1771
|
default: argvDefault,
|
|
1772
|
+
boolean: booleanArgs
|
|
1177
1773
|
})
|
|
1178
1774
|
|
|
1179
1775
|
// Now deactivate the inputs present in argv
|
|
@@ -1204,6 +1800,17 @@ Documentation: https://jsee.org
|
|
|
1204
1800
|
schema.model = [schema.model]
|
|
1205
1801
|
}
|
|
1206
1802
|
|
|
1803
|
+
if (argv.run) {
|
|
1804
|
+
const data = Object.assign(
|
|
1805
|
+
{},
|
|
1806
|
+
getSchemaInputDefaults(schema),
|
|
1807
|
+
getDataFromArgv(schema, argv, true)
|
|
1808
|
+
)
|
|
1809
|
+
const result = await runSchemaOnce(schema, data, schemaPath, cwd, log)
|
|
1810
|
+
emitRunOutputs(schema, result, argv, outputs, process.cwd())
|
|
1811
|
+
return result
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1207
1814
|
// Resolve server-side execution mode
|
|
1208
1815
|
const hasOutputs = Array.isArray(outputs) ? outputs.length > 0 : Boolean(outputs)
|
|
1209
1816
|
let shouldExecute = argv.execute
|
|
@@ -1230,18 +1837,7 @@ Documentation: https://jsee.org
|
|
|
1230
1837
|
if (shouldExecute) {
|
|
1231
1838
|
await Promise.all(schema.model.map(async m => {
|
|
1232
1839
|
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})
|
|
1840
|
+
modelFuncs[m.name] = await loadCliModelFunction(m, schemaPath, cwd, log)
|
|
1245
1841
|
m.type = 'post'
|
|
1246
1842
|
m.url = `/${m.name}`
|
|
1247
1843
|
m.worker = false
|
|
@@ -1285,7 +1881,8 @@ Documentation: https://jsee.org
|
|
|
1285
1881
|
|
|
1286
1882
|
// Generate description block
|
|
1287
1883
|
if (description) {
|
|
1288
|
-
const
|
|
1884
|
+
const descriptionPath = path.isAbsolute(description) ? description : path.join(cwd, description)
|
|
1885
|
+
const descriptionMd = fs.readFileSync(descriptionPath, 'utf8')
|
|
1289
1886
|
descriptionHtml = getMarkdownConverter().render(descriptionMd)
|
|
1290
1887
|
|
|
1291
1888
|
if (descriptionMd.includes('---')) {
|
|
@@ -1455,20 +2052,17 @@ Documentation: https://jsee.org
|
|
|
1455
2052
|
|
|
1456
2053
|
}
|
|
1457
2054
|
|
|
1458
|
-
// Build
|
|
1459
|
-
let
|
|
2055
|
+
// Build right-side header status/action. Serve mode shows only runtime status; bundled files can download themselves.
|
|
2056
|
+
let headerRightHtml = ''
|
|
1460
2057
|
let schemaScript = ''
|
|
1461
2058
|
const isServing = !hasOutputs
|
|
1462
2059
|
if (isServing) {
|
|
1463
2060
|
const toggleHtml = shouldExecute
|
|
1464
|
-
? '<label
|
|
2061
|
+
? '<label class="jsee-header-toggle"><input type="checkbox" id="exec-toggle">Browser</label>'
|
|
1465
2062
|
: ''
|
|
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>`
|
|
2063
|
+
headerRightHtml = `<div class="jsee-header-actions">${toggleHtml}<span class="jsee-header-badge">localhost:${argv.port}</span></div>`
|
|
2064
|
+
} else if (shouldBundle) {
|
|
2065
|
+
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
2066
|
}
|
|
1473
2067
|
if (shouldExecute) {
|
|
1474
2068
|
const clientSchema = JSON.parse(JSON.stringify(schema))
|
|
@@ -1486,7 +2080,7 @@ Documentation: https://jsee.org
|
|
|
1486
2080
|
hiddenElementHtml: hiddenElementHtml,
|
|
1487
2081
|
socialHtml: pad(socialHtml, 2, 1),
|
|
1488
2082
|
orgHtml: pad(orgHtml, 2, 1),
|
|
1489
|
-
|
|
2083
|
+
headerRightHtml: headerRightHtml,
|
|
1490
2084
|
schemaScript: schemaScript,
|
|
1491
2085
|
})
|
|
1492
2086
|
|
|
@@ -1499,11 +2093,11 @@ Documentation: https://jsee.org
|
|
|
1499
2093
|
if (o === 'stdout') {
|
|
1500
2094
|
log(html)
|
|
1501
2095
|
} else if (o.includes('.html')) {
|
|
1502
|
-
|
|
2096
|
+
writeOutputFile(cwd, o, html)
|
|
1503
2097
|
} else if (o.includes('.json')) {
|
|
1504
|
-
|
|
2098
|
+
writeOutputFile(cwd, o, JSON.stringify(schema, null, 2))
|
|
1505
2099
|
} else if (o.includes('.md')) {
|
|
1506
|
-
|
|
2100
|
+
writeOutputFile(cwd, o, genMarkdownFromSchema(schema))
|
|
1507
2101
|
} else {
|
|
1508
2102
|
console.error('Invalid output file:', o)
|
|
1509
2103
|
}
|
|
@@ -1613,6 +2207,16 @@ module.exports.resolveLocalImportFile = resolveLocalImportFile
|
|
|
1613
2207
|
module.exports.resolveFetchImport = resolveFetchImport
|
|
1614
2208
|
module.exports.resolveRuntimeMode = resolveRuntimeMode
|
|
1615
2209
|
module.exports.resolveOutputPath = resolveOutputPath
|
|
2210
|
+
module.exports.resolveJseePackageInput = resolveJseePackageInput
|
|
2211
|
+
module.exports.readJseePackageInput = readJseePackageInput
|
|
2212
|
+
module.exports.findPackageRoot = findPackageRoot
|
|
2213
|
+
module.exports.runPackage = runPackage
|
|
2214
|
+
module.exports.looksLikeMissingPackageInput = looksLikeMissingPackageInput
|
|
2215
|
+
module.exports.getPackageInputInstallHint = getPackageInputInstallHint
|
|
2216
|
+
module.exports.isPackageSpecifier = isPackageSpecifier
|
|
2217
|
+
module.exports.writeOutputFile = writeOutputFile
|
|
1616
2218
|
module.exports.needsFullBundle = needsFullBundle
|
|
1617
2219
|
module.exports.shouldBundleModelCode = shouldBundleModelCode
|
|
1618
2220
|
module.exports.bundleModelCode = bundleModelCode
|
|
2221
|
+
module.exports.runSchemaOnce = runSchemaOnce
|
|
2222
|
+
module.exports.emitRunOutputs = emitRunOutputs
|