@jseeio/jsee 0.8.1 → 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 +46 -16
- package/dist/jsee.core.js +1 -1
- package/dist/jsee.full.js +1 -1
- package/dist/jsee.runtime.js +1 -1
- package/package.json +4 -1
- package/src/cli.js +774 -55
- package/src/main.js +7 -0
- package/src/utils.js +38 -0
- package/templates/common-outputs.js +30 -4
- 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,510 @@ 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
|
+
|
|
790
|
+
let optionalEsbuild
|
|
791
|
+
function getOptionalEsbuild () {
|
|
792
|
+
if (optionalEsbuild) return optionalEsbuild
|
|
793
|
+
try {
|
|
794
|
+
optionalEsbuild = require('esbuild')
|
|
795
|
+
return optionalEsbuild
|
|
796
|
+
} catch (error) {
|
|
797
|
+
throw new Error('Bundling model dependencies requires optional dependency esbuild. Run `npm install esbuild` or use schema imports/prebundled browser code.')
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function isJsIdentifier (value) {
|
|
802
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value)
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function toJsIdentifier (value, fallback='model') {
|
|
806
|
+
const raw = value ? String(value) : fallback
|
|
807
|
+
const clean = raw.replace(/[^A-Za-z0-9_$]/g, '_')
|
|
808
|
+
if (!clean) return fallback
|
|
809
|
+
return /^[A-Za-z_$]/.test(clean) ? clean : '_' + clean
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function getModelExportName (model) {
|
|
813
|
+
if (model.name) return model.name
|
|
814
|
+
if (model.url) return path.basename(model.url).replace(/\.[^.]+$/, '')
|
|
815
|
+
return 'model'
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function hasCommonJsExport (code) {
|
|
819
|
+
return /\bmodule\s*\.\s*exports\b|\bexports\s*\.\s*[A-Za-z_$]/.test(code)
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function hasEsModuleExport (code) {
|
|
823
|
+
return /^\s*export\s+(?:default|function|class|const|let|var|\{)/m.test(code)
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function hasStaticImport (code) {
|
|
827
|
+
return /^\s*import\s+(?!\()/m.test(code)
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function hasRequireCall (code) {
|
|
831
|
+
return /\brequire\s*\(/.test(code)
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function shouldBundleModelCode (code) {
|
|
835
|
+
return hasRequireCall(code) || hasStaticImport(code) || hasCommonJsExport(code) || hasEsModuleExport(code)
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function buildBundledModelExposeCode (bundleGlobalName, modelName) {
|
|
839
|
+
return `
|
|
840
|
+
;(function () {
|
|
841
|
+
var mod = ${bundleGlobalName}
|
|
842
|
+
var target = mod
|
|
843
|
+
if (mod && (typeof mod === 'object')) {
|
|
844
|
+
if (typeof mod[${JSON.stringify(modelName)}] !== 'undefined') {
|
|
845
|
+
target = mod[${JSON.stringify(modelName)}]
|
|
846
|
+
} else if (typeof mod.default !== 'undefined') {
|
|
847
|
+
target = mod.default
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
globalThis[${JSON.stringify(modelName)}] = target
|
|
851
|
+
})()
|
|
852
|
+
`
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
async function bundleModelCode (model, modelPath, code) {
|
|
856
|
+
if (!shouldBundleModelCode(code)) return code
|
|
857
|
+
|
|
858
|
+
const esbuild = getOptionalEsbuild()
|
|
859
|
+
const modelName = getModelExportName(model)
|
|
860
|
+
const bundleGlobalName = `__jsee_bundle_${toJsIdentifier(modelName)}`
|
|
861
|
+
const needsExportShim = modelName &&
|
|
862
|
+
!hasCommonJsExport(code) &&
|
|
863
|
+
!hasEsModuleExport(code) &&
|
|
864
|
+
(hasRequireCall(code) || hasStaticImport(code))
|
|
865
|
+
|
|
866
|
+
if (needsExportShim && !isJsIdentifier(modelName)) {
|
|
867
|
+
throw new Error(`Bundling ${model.url || modelPath} requires a valid JavaScript model name or an explicit module export.`)
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const options = {
|
|
871
|
+
bundle: true,
|
|
872
|
+
write: false,
|
|
873
|
+
platform: 'browser',
|
|
874
|
+
format: 'iife',
|
|
875
|
+
globalName: bundleGlobalName,
|
|
876
|
+
logLevel: 'silent'
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (needsExportShim) {
|
|
880
|
+
options.stdin = {
|
|
881
|
+
contents: `${code}\nexport default ${modelName}\n`,
|
|
882
|
+
resolveDir: path.dirname(modelPath),
|
|
883
|
+
sourcefile: path.basename(modelPath),
|
|
884
|
+
loader: 'js'
|
|
885
|
+
}
|
|
886
|
+
} else {
|
|
887
|
+
options.entryPoints = [modelPath]
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
try {
|
|
891
|
+
const result = await esbuild.build(options)
|
|
892
|
+
return result.outputFiles[0].text + buildBundledModelExposeCode(bundleGlobalName, modelName)
|
|
893
|
+
} catch (error) {
|
|
894
|
+
throw new Error(`Failed to bundle ${model.url || modelPath}: ${error.message}`)
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
262
898
|
const FULL_BUNDLE_TYPES = ['chart', '3d', 'map']
|
|
263
899
|
|
|
264
900
|
function needsFullBundle (schema) {
|
|
@@ -596,10 +1232,18 @@ function template(schema, blocks) {
|
|
|
596
1232
|
table th { background-color: #f0f0f0; border: 1px solid #e0e0e0; }
|
|
597
1233
|
table td { border: 1px solid #e8e8e8; }
|
|
598
1234
|
@media screen and (max-width: 800px) { table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; -ms-overflow-style: -ms-autohiding-scrollbar; } }
|
|
599
|
-
.site-header { border-bottom: 1px solid #e8e8e8; min-height: 55.95px;
|
|
600
|
-
.site-
|
|
601
|
-
|
|
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; }
|
|
602
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; } }
|
|
603
1247
|
.site-nav { position: absolute; top: 9px; right: 15px; background-color: #fdfdfd; border: 1px solid #e8e8e8; border-radius: 5px; text-align: right; }
|
|
604
1248
|
.site-nav .nav-trigger { display: none; }
|
|
605
1249
|
.site-nav .menu-icon { float: right; width: 36px; height: 26px; line-height: 0; padding-top: 10px; text-align: center; }
|
|
@@ -659,7 +1303,6 @@ function template(schema, blocks) {
|
|
|
659
1303
|
@media screen and (min-width: 800px) { .one-half { width: calc(50% - (30px / 2)); } }
|
|
660
1304
|
/** Jsee elements */
|
|
661
1305
|
.app-container { background-color: #F0F1F4; border-bottom: 1px solid #e8e8e8; padding-bottom: 55px }
|
|
662
|
-
#save-html-btn:hover { background-color: #f0f0f0 !important; }
|
|
663
1306
|
.schema-description { background-color: #f8f8fa; padding: 20px; margin-top: 20px; border-radius: 10px; border: 1px solid #e8e8e8; }
|
|
664
1307
|
.schema-description h2, .schema-description h3, .schema-description h4 { margin-top: 10px; }
|
|
665
1308
|
/** Logos */
|
|
@@ -674,10 +1317,10 @@ function template(schema, blocks) {
|
|
|
674
1317
|
</head>
|
|
675
1318
|
<body>
|
|
676
1319
|
${blocks.hiddenElementHtml}
|
|
677
|
-
${blocks.serveBarHtml}
|
|
678
1320
|
<header class="site-header">
|
|
679
1321
|
<div class="wrapper">
|
|
680
1322
|
<span class="site-title">${title}</span>
|
|
1323
|
+
${blocks.headerRightHtml}
|
|
681
1324
|
</div>
|
|
682
1325
|
</header>
|
|
683
1326
|
<div class="page-content app-container">
|
|
@@ -728,9 +1371,39 @@ function template(schema, blocks) {
|
|
|
728
1371
|
env = new JSEE({ container: container, schema: currentSchema })
|
|
729
1372
|
})
|
|
730
1373
|
}
|
|
731
|
-
var
|
|
732
|
-
if (
|
|
733
|
-
|
|
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)
|
|
734
1407
|
}
|
|
735
1408
|
</script>
|
|
736
1409
|
</body>
|
|
@@ -751,25 +1424,29 @@ async function gen (pargv, returnHtml=false) {
|
|
|
751
1424
|
description: 'd',
|
|
752
1425
|
port: 'p',
|
|
753
1426
|
version: 'v',
|
|
754
|
-
fetch: 'f',
|
|
1427
|
+
fetch: ['f', 'bundle', 'b'],
|
|
755
1428
|
execute: 'e',
|
|
756
1429
|
cdn: 'c',
|
|
757
1430
|
runtime: 'r',
|
|
1431
|
+
serve: 's'
|
|
758
1432
|
}
|
|
759
1433
|
const argvDefault = {
|
|
760
1434
|
execute: 'auto', // execute the model code on the server (auto = server-side when serving local .js models)
|
|
761
|
-
fetch: false, //
|
|
1435
|
+
fetch: false, // bundle runtime, local code and imports into generated output
|
|
762
1436
|
inputs: 'schema.json', // default input is schema.json in the current working directory
|
|
763
1437
|
port: 3000, // default port for the server
|
|
764
1438
|
version: 'latest', // default version of JSEE runtime to use
|
|
765
1439
|
verbose: false, // verbose mode
|
|
766
1440
|
cdn: false,
|
|
767
|
-
runtime: 'auto'
|
|
1441
|
+
runtime: 'auto',
|
|
1442
|
+
serve: false,
|
|
1443
|
+
run: false
|
|
768
1444
|
}
|
|
1445
|
+
const booleanArgs = ['help', 'h', 'html', 'client', 'serve', 'run']
|
|
769
1446
|
let argv = minimist(pargv, {
|
|
770
1447
|
alias: argvAlias,
|
|
771
1448
|
default: argvDefault,
|
|
772
|
-
boolean:
|
|
1449
|
+
boolean: booleanArgs,
|
|
773
1450
|
})
|
|
774
1451
|
|
|
775
1452
|
// ── jsee init <template> ──────────────────────────────────────────
|
|
@@ -907,7 +1584,7 @@ npx @jseeio/jsee schema.json
|
|
|
907
1584
|
|
|
908
1585
|
if (argv.help || argv.h) {
|
|
909
1586
|
console.log(`
|
|
910
|
-
Usage: jsee [schema.json] [data...] [options]
|
|
1587
|
+
Usage: jsee [schema.json|package] [data...] [options]
|
|
911
1588
|
jsee init [template] [--html]
|
|
912
1589
|
|
|
913
1590
|
Commands:
|
|
@@ -920,7 +1597,10 @@ Options:
|
|
|
920
1597
|
-d, --description <file> Markdown description file to include
|
|
921
1598
|
-p, --port <number> Dev server port (default: 3000)
|
|
922
1599
|
-v, --version <version> JSEE runtime version (default: latest)
|
|
923
|
-
-
|
|
1600
|
+
-b, --bundle Bundle runtime + dependencies into output
|
|
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
|
|
924
1604
|
-e, --execute Execute model server-side (auto-enabled when serving local .js models)
|
|
925
1605
|
--client Force client-side execution (disable auto server-side)
|
|
926
1606
|
-c, --cdn <url|bool> Rewrite model URLs for CDN deployment
|
|
@@ -938,11 +1618,14 @@ Examples:
|
|
|
938
1618
|
jsee init chat Scaffold chat project
|
|
939
1619
|
jsee init --html Generate single index.html
|
|
940
1620
|
jsee schema.json Start dev server with schema
|
|
1621
|
+
jsee @scope/app --serve Start dev server from a JSEE app package
|
|
941
1622
|
jsee schema.json 42 hello Pass positional data to first two inputs
|
|
942
1623
|
jsee schema.json --a=100 --b=200 Pass named data inputs
|
|
943
1624
|
jsee schema.json data.csv Pass a file path as input
|
|
944
1625
|
jsee schema.json -o app.html Generate static HTML file
|
|
945
|
-
jsee schema.json -o app.html
|
|
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
|
|
946
1629
|
jsee -p 8080 Start dev server on port 8080
|
|
947
1630
|
jsee report.pdf Serve a PDF file (auto-detected viewer)
|
|
948
1631
|
jsee data/ Serve a folder (file browser with preview)
|
|
@@ -952,14 +1635,18 @@ Documentation: https://jsee.org
|
|
|
952
1635
|
return
|
|
953
1636
|
}
|
|
954
1637
|
|
|
955
|
-
// Check if first positional arg is a file or
|
|
1638
|
+
// Check if first positional arg is a file/directory or JSEE app package.
|
|
956
1639
|
let identityResult = null
|
|
1640
|
+
let packageInput = null
|
|
957
1641
|
if (!imported && argv._.length > 0) {
|
|
958
1642
|
identityResult = generateIdentitySchema(argv._[0], process.cwd())
|
|
1643
|
+
if (!identityResult) {
|
|
1644
|
+
packageInput = resolveJseePackageInput(argv._[0], process.cwd())
|
|
1645
|
+
}
|
|
959
1646
|
}
|
|
960
1647
|
|
|
961
|
-
// Set argv.inputs to the first positional argument if it looks like a schema/script
|
|
962
|
-
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) {
|
|
963
1650
|
argv.inputs = argv._[0]
|
|
964
1651
|
}
|
|
965
1652
|
|
|
@@ -978,8 +1665,22 @@ Documentation: https://jsee.org
|
|
|
978
1665
|
|
|
979
1666
|
let cwd = process.cwd()
|
|
980
1667
|
let inputs = argv.inputs
|
|
981
|
-
let outputs = argv.outputs
|
|
982
|
-
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
|
+
}
|
|
983
1684
|
let schema
|
|
984
1685
|
let schemaPath
|
|
985
1686
|
let descriptionTxt = ''
|
|
@@ -990,13 +1691,13 @@ Documentation: https://jsee.org
|
|
|
990
1691
|
// Determine the inputs and outputs
|
|
991
1692
|
// if inputs is a string with js file names, split it into an array
|
|
992
1693
|
if (typeof inputs === 'string') {
|
|
993
|
-
if (inputs.includes('.js')) {
|
|
1694
|
+
if (inputs.includes('.js') || inputs.includes('.json') || inputs.includes(',')) {
|
|
994
1695
|
inputs = inputs.split(',')
|
|
995
1696
|
}
|
|
996
1697
|
}
|
|
997
1698
|
|
|
998
1699
|
// if outputs is a string with js file names, split it into an array
|
|
999
|
-
if (typeof outputs === 'string') {
|
|
1700
|
+
if (typeof outputs === 'string' && !argv.run) {
|
|
1000
1701
|
outputs = outputs.split(',')
|
|
1001
1702
|
}
|
|
1002
1703
|
|
|
@@ -1041,8 +1742,11 @@ Documentation: https://jsee.org
|
|
|
1041
1742
|
schema.inputs.forEach((inp, inp_index) => {
|
|
1042
1743
|
if (inp.name) {
|
|
1043
1744
|
const inputName = sanitizeName(inp.name)
|
|
1044
|
-
|
|
1045
|
-
|
|
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
|
|
1046
1750
|
}
|
|
1047
1751
|
// Use positional arguments as schema input defaults
|
|
1048
1752
|
if (imported && argv._.length > inp_index) {
|
|
@@ -1065,6 +1769,7 @@ Documentation: https://jsee.org
|
|
|
1065
1769
|
argv = minimist(pargv, {
|
|
1066
1770
|
alias: argvAlias,
|
|
1067
1771
|
default: argvDefault,
|
|
1772
|
+
boolean: booleanArgs
|
|
1068
1773
|
})
|
|
1069
1774
|
|
|
1070
1775
|
// Now deactivate the inputs present in argv
|
|
@@ -1083,7 +1788,7 @@ Documentation: https://jsee.org
|
|
|
1083
1788
|
}
|
|
1084
1789
|
log('Argv:', argv)
|
|
1085
1790
|
|
|
1086
|
-
//
|
|
1791
|
+
// Bundle/offline generation
|
|
1087
1792
|
// Check if schema has model, convert to array if needed
|
|
1088
1793
|
if (!schema.model) {
|
|
1089
1794
|
// console.error('No model found in schema')
|
|
@@ -1095,6 +1800,17 @@ Documentation: https://jsee.org
|
|
|
1095
1800
|
schema.model = [schema.model]
|
|
1096
1801
|
}
|
|
1097
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
|
+
|
|
1098
1814
|
// Resolve server-side execution mode
|
|
1099
1815
|
const hasOutputs = Array.isArray(outputs) ? outputs.length > 0 : Boolean(outputs)
|
|
1100
1816
|
let shouldExecute = argv.execute
|
|
@@ -1121,18 +1837,7 @@ Documentation: https://jsee.org
|
|
|
1121
1837
|
if (shouldExecute) {
|
|
1122
1838
|
await Promise.all(schema.model.map(async m => {
|
|
1123
1839
|
log('Preparing a model to run on the server side:', m.name, m.url)
|
|
1124
|
-
|
|
1125
|
-
let target = require(modelPath)
|
|
1126
|
-
// Handle browser-style model files (no module.exports) — eval the source
|
|
1127
|
-
// to extract the named function, similar to how the worker does it
|
|
1128
|
-
if (typeof target !== 'function' && (!target || Object.keys(target).length === 0)) {
|
|
1129
|
-
const src = fs.readFileSync(modelPath, 'utf-8')
|
|
1130
|
-
const fn = new Function(src + `\nreturn typeof ${m.name} === 'function' ? ${m.name} : undefined`)()
|
|
1131
|
-
if (typeof fn === 'function') {
|
|
1132
|
-
target = fn
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
modelFuncs[m.name] = await getModelFuncJS(m, target, {log})
|
|
1840
|
+
modelFuncs[m.name] = await loadCliModelFunction(m, schemaPath, cwd, log)
|
|
1136
1841
|
m.type = 'post'
|
|
1137
1842
|
m.url = `/${m.name}`
|
|
1138
1843
|
m.worker = false
|
|
@@ -1176,7 +1881,8 @@ Documentation: https://jsee.org
|
|
|
1176
1881
|
|
|
1177
1882
|
// Generate description block
|
|
1178
1883
|
if (description) {
|
|
1179
|
-
const
|
|
1884
|
+
const descriptionPath = path.isAbsolute(description) ? description : path.join(cwd, description)
|
|
1885
|
+
const descriptionMd = fs.readFileSync(descriptionPath, 'utf8')
|
|
1180
1886
|
descriptionHtml = getMarkdownConverter().render(descriptionMd)
|
|
1181
1887
|
|
|
1182
1888
|
if (descriptionMd.includes('---')) {
|
|
@@ -1194,8 +1900,9 @@ Documentation: https://jsee.org
|
|
|
1194
1900
|
// Generate jsee code
|
|
1195
1901
|
let jseeHtml = ''
|
|
1196
1902
|
let hiddenElementHtml = ''
|
|
1197
|
-
const
|
|
1198
|
-
|
|
1903
|
+
const shouldBundle = Boolean(argv.fetch)
|
|
1904
|
+
const runtimeMode = resolveRuntimeMode(argv.runtime, shouldBundle, hasOutputs)
|
|
1905
|
+
if (shouldBundle) {
|
|
1199
1906
|
// Fetch jsee code from the CDN or local server
|
|
1200
1907
|
const jseeCode = await loadRuntimeCode(argv.version, schema)
|
|
1201
1908
|
jseeHtml = `<script>${jseeCode}</script>`
|
|
@@ -1209,7 +1916,10 @@ Documentation: https://jsee.org
|
|
|
1209
1916
|
}
|
|
1210
1917
|
if (m.url) {
|
|
1211
1918
|
// Fetch model from the local file system (remote URLs not yet supported here)
|
|
1212
|
-
const
|
|
1919
|
+
const modelPath = path.join(cwd, m.url)
|
|
1920
|
+
if (!m.name) m.name = getModelExportName(m)
|
|
1921
|
+
const modelSource = fs.readFileSync(modelPath, 'utf8')
|
|
1922
|
+
const modelCode = await bundleModelCode(m, modelPath, modelSource)
|
|
1213
1923
|
hiddenElementHtml += `<script type="text/plain" style="display: none;" data-src="${m.url}">${modelCode}</script>`
|
|
1214
1924
|
}
|
|
1215
1925
|
const imports = toArray(m.imports)
|
|
@@ -1342,20 +2052,17 @@ Documentation: https://jsee.org
|
|
|
1342
2052
|
|
|
1343
2053
|
}
|
|
1344
2054
|
|
|
1345
|
-
// Build
|
|
1346
|
-
let
|
|
2055
|
+
// Build right-side header status/action. Serve mode shows only runtime status; bundled files can download themselves.
|
|
2056
|
+
let headerRightHtml = ''
|
|
1347
2057
|
let schemaScript = ''
|
|
1348
2058
|
const isServing = !hasOutputs
|
|
1349
2059
|
if (isServing) {
|
|
1350
2060
|
const toggleHtml = shouldExecute
|
|
1351
|
-
? '<label
|
|
2061
|
+
? '<label class="jsee-header-toggle"><input type="checkbox" id="exec-toggle">Browser</label>'
|
|
1352
2062
|
: ''
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
${toggleHtml}
|
|
1357
|
-
<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>
|
|
1358
|
-
</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>'
|
|
1359
2066
|
}
|
|
1360
2067
|
if (shouldExecute) {
|
|
1361
2068
|
const clientSchema = JSON.parse(JSON.stringify(schema))
|
|
@@ -1373,7 +2080,7 @@ Documentation: https://jsee.org
|
|
|
1373
2080
|
hiddenElementHtml: hiddenElementHtml,
|
|
1374
2081
|
socialHtml: pad(socialHtml, 2, 1),
|
|
1375
2082
|
orgHtml: pad(orgHtml, 2, 1),
|
|
1376
|
-
|
|
2083
|
+
headerRightHtml: headerRightHtml,
|
|
1377
2084
|
schemaScript: schemaScript,
|
|
1378
2085
|
})
|
|
1379
2086
|
|
|
@@ -1386,11 +2093,11 @@ Documentation: https://jsee.org
|
|
|
1386
2093
|
if (o === 'stdout') {
|
|
1387
2094
|
log(html)
|
|
1388
2095
|
} else if (o.includes('.html')) {
|
|
1389
|
-
|
|
2096
|
+
writeOutputFile(cwd, o, html)
|
|
1390
2097
|
} else if (o.includes('.json')) {
|
|
1391
|
-
|
|
2098
|
+
writeOutputFile(cwd, o, JSON.stringify(schema, null, 2))
|
|
1392
2099
|
} else if (o.includes('.md')) {
|
|
1393
|
-
|
|
2100
|
+
writeOutputFile(cwd, o, genMarkdownFromSchema(schema))
|
|
1394
2101
|
} else {
|
|
1395
2102
|
console.error('Invalid output file:', o)
|
|
1396
2103
|
}
|
|
@@ -1500,4 +2207,16 @@ module.exports.resolveLocalImportFile = resolveLocalImportFile
|
|
|
1500
2207
|
module.exports.resolveFetchImport = resolveFetchImport
|
|
1501
2208
|
module.exports.resolveRuntimeMode = resolveRuntimeMode
|
|
1502
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
|
|
1503
2218
|
module.exports.needsFullBundle = needsFullBundle
|
|
2219
|
+
module.exports.shouldBundleModelCode = shouldBundleModelCode
|
|
2220
|
+
module.exports.bundleModelCode = bundleModelCode
|
|
2221
|
+
module.exports.runSchemaOnce = runSchemaOnce
|
|
2222
|
+
module.exports.emitRunOutputs = emitRunOutputs
|