@jseeio/jsee 0.4.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -0
- package/LICENSE +21 -0
- package/README.md +583 -55
- package/dist/2b3e1faf89f94a483539.png +0 -0
- package/dist/416d91365b44e4b4f477.png +0 -0
- package/dist/8f2c4d11474275fbc161.png +0 -0
- package/dist/jsee.core.js +1 -0
- package/dist/jsee.full.js +1 -0
- package/dist/jsee.runtime.js +2 -1
- package/package.json +84 -18
- package/src/app.js +130 -33
- package/src/cli.js +474 -64
- package/src/extended-imports.js +11 -0
- package/src/main.js +264 -45
- package/src/overlay.js +26 -1
- package/src/utils.js +390 -12
- package/templates/common-inputs.js +88 -0
- package/templates/common-outputs.js +340 -4
- package/templates/minimal-app.vue +367 -0
- package/templates/minimal-input.vue +573 -0
- package/templates/minimal-output.vue +426 -0
- package/templates/virtual-table.vue +194 -0
- package/.claude/settings.local.json +0 -13
- package/.eslintrc.js +0 -38
- package/AGENTS.md +0 -65
- package/CLAUDE.md +0 -5
- package/CNAME +0 -1
- package/_config.yml +0 -26
- package/dist/jsee.js +0 -1
- package/jest-puppeteer.config.js +0 -14
- package/jest.config.js +0 -8
- package/jest.unit.config.js +0 -8
- package/load/index.html +0 -52
- package/templates/bulma-app.vue +0 -242
- package/templates/bulma-input.vue +0 -125
- package/templates/bulma-output.vue +0 -101
- package/test/class.html +0 -22
- package/test/code.html +0 -25
- package/test/codew.html +0 -25
- package/test/example-class.js +0 -8
- package/test/example-sum.js +0 -3
- package/test/fixtures/lodash-like.js +0 -15
- package/test/fixtures/upload-sample.csv +0 -3
- package/test/importw.html +0 -28
- package/test/minimal.html +0 -14
- package/test/minimal1.html +0 -13
- package/test/minimal2.html +0 -15
- package/test/minimal3.html +0 -10
- package/test/minimal4.html +0 -22
- package/test/pipeline.html +0 -52
- package/test/python.html +0 -41
- package/test/string.html +0 -26
- package/test/stringw.html +0 -29
- package/test/sum.schema.json +0 -17
- package/test/sumw.schema.json +0 -15
- package/test/test-basic.test.js +0 -603
- package/test/test-python.test.js +0 -23
- package/test/unit/cli-fetch.test.js +0 -229
- package/test/unit/utils.test.js +0 -888
- package/webpack.config.js +0 -101
package/src/cli.js
CHANGED
|
@@ -4,23 +4,27 @@ const os = require('os')
|
|
|
4
4
|
const crypto = require('crypto')
|
|
5
5
|
|
|
6
6
|
const minimist = require('minimist')
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
7
|
+
|
|
8
|
+
const { getModelFuncJS, sanitizeName, generateOpenAPISpec, serializeResult, parseMultipart, fileExtToOutputType } = require('./utils.js')
|
|
9
|
+
|
|
10
|
+
let jsdoc2md
|
|
11
|
+
function getJsdocToMarkdown () {
|
|
12
|
+
if (!jsdoc2md) jsdoc2md = require('jsdoc-to-markdown')
|
|
13
|
+
return jsdoc2md
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let markdownConverter
|
|
17
|
+
function getMarkdownConverter () {
|
|
18
|
+
if (markdownConverter) return markdownConverter
|
|
19
|
+
|
|
20
|
+
const MarkdownIt = require('markdown-it')
|
|
21
|
+
markdownConverter = new MarkdownIt({
|
|
22
|
+
html: true,
|
|
23
|
+
linkify: true,
|
|
24
|
+
typographer: false
|
|
25
|
+
})
|
|
26
|
+
return markdownConverter
|
|
27
|
+
}
|
|
24
28
|
|
|
25
29
|
// left padding of multiple lines
|
|
26
30
|
function pad (str, len, start=0) {
|
|
@@ -36,6 +40,95 @@ function toArray (value) {
|
|
|
36
40
|
return Array.isArray(value) ? value : [value]
|
|
37
41
|
}
|
|
38
42
|
|
|
43
|
+
// Auto-detect CLI argument type: file path, number, JSON array/object, or string
|
|
44
|
+
function detectArgValue (value) {
|
|
45
|
+
if (typeof value === 'number') return value
|
|
46
|
+
if (typeof value === 'boolean') return value
|
|
47
|
+
if (typeof value !== 'string') return value
|
|
48
|
+
// File on disk
|
|
49
|
+
if (fs.existsSync(value) && fs.statSync(value).isFile()) return value
|
|
50
|
+
// Number
|
|
51
|
+
if (value !== '' && !isNaN(value)) return Number(value)
|
|
52
|
+
// JSON array or object
|
|
53
|
+
if (value.startsWith('[') || value.startsWith('{')) {
|
|
54
|
+
try { return JSON.parse(value) } catch (e) { /* not JSON */ }
|
|
55
|
+
}
|
|
56
|
+
return value
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readDirListing (resolved, relative) {
|
|
60
|
+
const entries = fs.readdirSync(resolved, { withFileTypes: true })
|
|
61
|
+
return entries
|
|
62
|
+
.filter(e => e.isFile() && !e.name.startsWith('.'))
|
|
63
|
+
.map(e => ({
|
|
64
|
+
name: e.name,
|
|
65
|
+
path: path.posix.join(relative, e.name),
|
|
66
|
+
size: fs.statSync(path.join(resolved, e.name)).size,
|
|
67
|
+
type: fileExtToOutputType(e.name),
|
|
68
|
+
selected: true
|
|
69
|
+
}))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function generateIdentitySchema (target, cwd) {
|
|
73
|
+
const resolved = path.isAbsolute(target) ? target : path.join(cwd, target)
|
|
74
|
+
const stat = fs.existsSync(resolved) ? fs.statSync(resolved) : null
|
|
75
|
+
|
|
76
|
+
if (stat && stat.isDirectory()) {
|
|
77
|
+
const files = readDirListing(resolved, target)
|
|
78
|
+
// Pre-select the first file
|
|
79
|
+
files.forEach((f, i) => { f.selected = i === 0 })
|
|
80
|
+
return {
|
|
81
|
+
schema: {
|
|
82
|
+
model: {
|
|
83
|
+
name: 'identity',
|
|
84
|
+
type: 'function',
|
|
85
|
+
worker: false,
|
|
86
|
+
autorun: true,
|
|
87
|
+
code: `function identity(inputs) {
|
|
88
|
+
var files = inputs.files || []
|
|
89
|
+
var selected = files.filter(function (f) { return f.selected })
|
|
90
|
+
var stats = { files: inputs.files ? inputs.files.length : 0, totalSize: (inputs.files || []).reduce(function (s, f) { return s + (f.size || 0) }, 0), types: {} }
|
|
91
|
+
;(inputs.files || []).forEach(function (f) { var t = f.type || 'unknown'; stats.types[t] = (stats.types[t] || 0) + 1 })
|
|
92
|
+
return { stats: stats, viewer: selected.length > 0 ? '/__jsee/file?path=' + encodeURIComponent(selected[0].path) : null }
|
|
93
|
+
}`
|
|
94
|
+
},
|
|
95
|
+
inputs: [{
|
|
96
|
+
name: 'files',
|
|
97
|
+
type: 'folder',
|
|
98
|
+
default: files,
|
|
99
|
+
disabled: true,
|
|
100
|
+
reactive: true,
|
|
101
|
+
select: 'one'
|
|
102
|
+
}],
|
|
103
|
+
outputs: [
|
|
104
|
+
{ name: 'stats', type: 'object' },
|
|
105
|
+
{ name: 'viewer', type: 'viewer' }
|
|
106
|
+
],
|
|
107
|
+
autorun: true
|
|
108
|
+
},
|
|
109
|
+
identity: true,
|
|
110
|
+
serveDir: resolved
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (stat && stat.isFile()) {
|
|
115
|
+
const ext = path.extname(target).toLowerCase()
|
|
116
|
+
if (ext === '.json' || ext === '.js') return null
|
|
117
|
+
const outputType = fileExtToOutputType(target)
|
|
118
|
+
return {
|
|
119
|
+
schema: {
|
|
120
|
+
inputs: [],
|
|
121
|
+
outputs: [{ name: path.basename(target), type: outputType }],
|
|
122
|
+
autorun: true
|
|
123
|
+
},
|
|
124
|
+
identity: true,
|
|
125
|
+
serveFile: { path: target, resolved }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
|
|
39
132
|
function collectFetchBundleBlocks (schema) {
|
|
40
133
|
return []
|
|
41
134
|
.concat(toArray(schema.model))
|
|
@@ -166,14 +259,32 @@ function resolveOutputPath (cwd, outputPath) {
|
|
|
166
259
|
return path.join(cwd, outputPath)
|
|
167
260
|
}
|
|
168
261
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
262
|
+
const FULL_BUNDLE_TYPES = ['chart', '3d', 'map']
|
|
263
|
+
|
|
264
|
+
function needsFullBundle (schema) {
|
|
265
|
+
const outputs = schema.outputs || []
|
|
266
|
+
const check = (list) => list.some(o => {
|
|
267
|
+
if (FULL_BUNDLE_TYPES.includes(o.type)) return true
|
|
268
|
+
if (o.type === 'group' && o.elements) return check(o.elements)
|
|
269
|
+
return false
|
|
270
|
+
})
|
|
271
|
+
return check(outputs)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function getBundleFilename (schema) {
|
|
275
|
+
return needsFullBundle(schema) ? 'jsee.full.js' : 'jsee.core.js'
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function loadRuntimeCode (version, schema) {
|
|
279
|
+
const bundle = schema ? getBundleFilename(schema) : 'jsee.core.js'
|
|
280
|
+
if (version === 'dev' || version === 'latest') {
|
|
281
|
+
const filePath = path.join(__dirname, '..', 'dist', bundle)
|
|
282
|
+
if (fs.existsSync(filePath)) {
|
|
283
|
+
return fs.readFileSync(filePath, 'utf8')
|
|
284
|
+
}
|
|
285
|
+
return fs.readFileSync(path.join(__dirname, '..', 'dist', 'jsee.core.js'), 'utf8')
|
|
175
286
|
}
|
|
176
|
-
const response = await fetch(`https://cdn.jsdelivr.net/npm/@jseeio/jsee@${version}/dist
|
|
287
|
+
const response = await fetch(`https://cdn.jsdelivr.net/npm/@jseeio/jsee@${version}/dist/${bundle}`)
|
|
177
288
|
return response.text()
|
|
178
289
|
}
|
|
179
290
|
|
|
@@ -182,7 +293,6 @@ function getDataFromArgv (schema, argv, loadFiles=true) {
|
|
|
182
293
|
if (schema.inputs) {
|
|
183
294
|
schema.inputs.forEach(inp => {
|
|
184
295
|
const inputName = sanitizeName(inp.name)
|
|
185
|
-
console.log('Processing input:', inp.name, 'as', inputName)
|
|
186
296
|
if (inputName in argv) {
|
|
187
297
|
switch (inp.type) {
|
|
188
298
|
case 'file':
|
|
@@ -197,6 +307,17 @@ function getDataFromArgv (schema, argv, loadFiles=true) {
|
|
|
197
307
|
process.exit(1)
|
|
198
308
|
}
|
|
199
309
|
break
|
|
310
|
+
case 'folder':
|
|
311
|
+
if (!loadFiles) {
|
|
312
|
+
data[inp.name] = argv[inputName]
|
|
313
|
+
break
|
|
314
|
+
}
|
|
315
|
+
const dirPath = argv[inputName]
|
|
316
|
+
const resolved = path.isAbsolute(dirPath) ? dirPath : path.join(process.cwd(), dirPath)
|
|
317
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
318
|
+
data[inp.name] = readDirListing(resolved, dirPath)
|
|
319
|
+
}
|
|
320
|
+
break
|
|
200
321
|
case 'int':
|
|
201
322
|
data[inp.name] = parseInt(argv[inputName], 10)
|
|
202
323
|
break
|
|
@@ -393,9 +514,9 @@ function template(schema, blocks) {
|
|
|
393
514
|
} else if (schema.page && schema.page.title) {
|
|
394
515
|
title = schema.page.title
|
|
395
516
|
} else if (schema.model) {
|
|
396
|
-
if (Array.isArray(schema.model)) {
|
|
517
|
+
if (Array.isArray(schema.model) && schema.model.length > 0) {
|
|
397
518
|
title = schema.model[0].name
|
|
398
|
-
} else {
|
|
519
|
+
} else if (!Array.isArray(schema.model)) {
|
|
399
520
|
title = schema.model.name
|
|
400
521
|
}
|
|
401
522
|
}
|
|
@@ -538,8 +659,7 @@ function template(schema, blocks) {
|
|
|
538
659
|
@media screen and (min-width: 800px) { .one-half { width: calc(50% - (30px / 2)); } }
|
|
539
660
|
/** Jsee elements */
|
|
540
661
|
.app-container { background-color: #F0F1F4; border-bottom: 1px solid #e8e8e8; padding-bottom: 55px }
|
|
541
|
-
#
|
|
542
|
-
#download-btn:hover { background-color: #f0f0f0; }
|
|
662
|
+
#save-html-btn:hover { background-color: #f0f0f0 !important; }
|
|
543
663
|
.schema-description { background-color: #f8f8fa; padding: 20px; margin-top: 20px; border-radius: 10px; border: 1px solid #e8e8e8; }
|
|
544
664
|
.schema-description h2, .schema-description h3, .schema-description h4 { margin-top: 10px; }
|
|
545
665
|
/** Logos */
|
|
@@ -554,10 +674,10 @@ function template(schema, blocks) {
|
|
|
554
674
|
</head>
|
|
555
675
|
<body>
|
|
556
676
|
${blocks.hiddenElementHtml}
|
|
677
|
+
${blocks.serveBarHtml}
|
|
557
678
|
<header class="site-header">
|
|
558
679
|
<div class="wrapper">
|
|
559
680
|
<span class="site-title">${title}</span>
|
|
560
|
-
<button id="download-btn" title="Download bundled HTML file without external dependencies to use offline">Download bundle (html)</button>
|
|
561
681
|
</div>
|
|
562
682
|
</header>
|
|
563
683
|
<div class="page-content app-container">
|
|
@@ -593,15 +713,25 @@ function template(schema, blocks) {
|
|
|
593
713
|
</footer>
|
|
594
714
|
${blocks.jseeHtml}
|
|
595
715
|
<script>
|
|
596
|
-
|
|
597
|
-
|
|
716
|
+
${blocks.schemaScript}
|
|
717
|
+
var currentSchema = schemaServer || schemaClient
|
|
718
|
+
var container = document.getElementById('jsee-container')
|
|
598
719
|
var env = new JSEE({
|
|
599
|
-
container:
|
|
600
|
-
schema
|
|
601
|
-
})
|
|
602
|
-
document.getElementById('download-btn').addEventListener('click', async () => {
|
|
603
|
-
env.download(title)
|
|
720
|
+
container: container,
|
|
721
|
+
schema: currentSchema
|
|
604
722
|
})
|
|
723
|
+
var toggle = document.getElementById('exec-toggle')
|
|
724
|
+
if (toggle) {
|
|
725
|
+
toggle.addEventListener('change', function () {
|
|
726
|
+
env.destroy()
|
|
727
|
+
currentSchema = this.checked ? schemaClient : schemaServer
|
|
728
|
+
env = new JSEE({ container: container, schema: currentSchema })
|
|
729
|
+
})
|
|
730
|
+
}
|
|
731
|
+
var saveBtn = document.getElementById('save-html-btn')
|
|
732
|
+
if (saveBtn) {
|
|
733
|
+
saveBtn.addEventListener('click', function () { env.download("${title}") })
|
|
734
|
+
}
|
|
605
735
|
</script>
|
|
606
736
|
</body>
|
|
607
737
|
</html>`
|
|
@@ -625,7 +755,7 @@ async function gen (pargv, returnHtml=false) {
|
|
|
625
755
|
runtime: 'r',
|
|
626
756
|
}
|
|
627
757
|
const argvDefault = {
|
|
628
|
-
execute:
|
|
758
|
+
execute: 'auto', // execute the model code on the server (auto = server-side when serving local .js models)
|
|
629
759
|
fetch: false, // fetch the JSEE runtime from the CDN or local server
|
|
630
760
|
inputs: 'schema.json', // default input is schema.json in the current working directory
|
|
631
761
|
port: 3000, // default port for the server
|
|
@@ -637,12 +767,150 @@ async function gen (pargv, returnHtml=false) {
|
|
|
637
767
|
let argv = minimist(pargv, {
|
|
638
768
|
alias: argvAlias,
|
|
639
769
|
default: argvDefault,
|
|
640
|
-
boolean: ['help', 'h'],
|
|
770
|
+
boolean: ['help', 'h', 'html', 'client'],
|
|
641
771
|
})
|
|
642
772
|
|
|
773
|
+
// ── jsee init <template> ──────────────────────────────────────────
|
|
774
|
+
if (argv._[0] === 'init') {
|
|
775
|
+
const tpl = argv._[1] || 'minimal'
|
|
776
|
+
const cwd = process.cwd()
|
|
777
|
+
|
|
778
|
+
if (argv.html) {
|
|
779
|
+
// Single index.html with inline schema + CDN script
|
|
780
|
+
const dest = path.join(cwd, 'index.html')
|
|
781
|
+
if (fs.existsSync(dest)) {
|
|
782
|
+
console.log('index.html already exists, skipping')
|
|
783
|
+
return
|
|
784
|
+
}
|
|
785
|
+
let htmlContent
|
|
786
|
+
if (tpl === 'chat') {
|
|
787
|
+
htmlContent = `<!DOCTYPE html>
|
|
788
|
+
<html lang="en">
|
|
789
|
+
<head>
|
|
790
|
+
<meta charset="utf-8">
|
|
791
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
792
|
+
<title>Chat</title>
|
|
793
|
+
</head>
|
|
794
|
+
<body>
|
|
795
|
+
<div id="jsee-container"></div>
|
|
796
|
+
<script src="https://cdn.jsdelivr.net/npm/@jseeio/jsee@latest/dist/jsee.core.js"></script>
|
|
797
|
+
<script>
|
|
798
|
+
new JSEE({
|
|
799
|
+
container: document.getElementById('jsee-container'),
|
|
800
|
+
schema: {
|
|
801
|
+
model: { code: function chat(message, history) { return { chat: 'You said: ' + message } }, worker: false },
|
|
802
|
+
inputs: [{ name: 'message', type: 'string', enter: true }],
|
|
803
|
+
outputs: [{ name: 'chat', type: 'chat' }]
|
|
804
|
+
}
|
|
805
|
+
})
|
|
806
|
+
</script>
|
|
807
|
+
</body>
|
|
808
|
+
</html>`
|
|
809
|
+
} else {
|
|
810
|
+
htmlContent = `<!DOCTYPE html>
|
|
811
|
+
<html lang="en">
|
|
812
|
+
<head>
|
|
813
|
+
<meta charset="utf-8">
|
|
814
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
815
|
+
<title>My App</title>
|
|
816
|
+
</head>
|
|
817
|
+
<body>
|
|
818
|
+
<div id="jsee-container"></div>
|
|
819
|
+
<script src="https://cdn.jsdelivr.net/npm/@jseeio/jsee@latest/dist/jsee.core.js"></script>
|
|
820
|
+
<script>
|
|
821
|
+
new JSEE({
|
|
822
|
+
container: document.getElementById('jsee-container'),
|
|
823
|
+
schema: {
|
|
824
|
+
model: { code: function greet(name, count) { return { greeting: (name + '\\n').repeat(count) } }, worker: false },
|
|
825
|
+
inputs: [
|
|
826
|
+
{ name: 'name', type: 'string', default: 'World' },
|
|
827
|
+
{ name: 'count', type: 'slider', min: 1, max: 10, default: 3 }
|
|
828
|
+
],
|
|
829
|
+
outputs: [{ name: 'greeting', type: 'code' }]
|
|
830
|
+
}
|
|
831
|
+
})
|
|
832
|
+
</script>
|
|
833
|
+
</body>
|
|
834
|
+
</html>`
|
|
835
|
+
}
|
|
836
|
+
fs.writeFileSync(dest, htmlContent)
|
|
837
|
+
console.log('Created index.html')
|
|
838
|
+
return
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Multi-file scaffolding
|
|
842
|
+
const files = {}
|
|
843
|
+
if (tpl === 'chat') {
|
|
844
|
+
files['schema.json'] = JSON.stringify({
|
|
845
|
+
model: { url: 'model.js', name: 'chat', worker: false },
|
|
846
|
+
inputs: [{ name: 'message', type: 'string', enter: true }],
|
|
847
|
+
outputs: [{ name: 'chat', type: 'chat' }]
|
|
848
|
+
}, null, 2) + '\n'
|
|
849
|
+
files['model.js'] = `function chat(message, history) {
|
|
850
|
+
return { chat: 'You said: ' + message }
|
|
851
|
+
}
|
|
852
|
+
`
|
|
853
|
+
files['README.md'] = `# Chat
|
|
854
|
+
|
|
855
|
+
A chat app built with [JSEE](https://jsee.org).
|
|
856
|
+
|
|
857
|
+
## Run
|
|
858
|
+
|
|
859
|
+
\`\`\`bash
|
|
860
|
+
npx @jseeio/jsee schema.json
|
|
861
|
+
\`\`\`
|
|
862
|
+
`
|
|
863
|
+
} else {
|
|
864
|
+
// minimal (default)
|
|
865
|
+
files['schema.json'] = JSON.stringify({
|
|
866
|
+
model: { url: 'model.js', name: 'greet' },
|
|
867
|
+
inputs: [
|
|
868
|
+
{ name: 'name', type: 'string', default: 'World' },
|
|
869
|
+
{ name: 'count', type: 'slider', min: 1, max: 10, default: 3 }
|
|
870
|
+
],
|
|
871
|
+
outputs: [{ name: 'greeting', type: 'code' }]
|
|
872
|
+
}, null, 2) + '\n'
|
|
873
|
+
files['model.js'] = `function greet(name, count) {
|
|
874
|
+
return { greeting: (name + '\\n').repeat(count) }
|
|
875
|
+
}
|
|
876
|
+
`
|
|
877
|
+
files['README.md'] = `# My App
|
|
878
|
+
|
|
879
|
+
A web app built with [JSEE](https://jsee.org).
|
|
880
|
+
|
|
881
|
+
## Run
|
|
882
|
+
|
|
883
|
+
\`\`\`bash
|
|
884
|
+
npx @jseeio/jsee schema.json
|
|
885
|
+
\`\`\`
|
|
886
|
+
`
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
let created = 0
|
|
890
|
+
for (const [filename, content] of Object.entries(files)) {
|
|
891
|
+
const dest = path.join(cwd, filename)
|
|
892
|
+
if (fs.existsSync(dest)) {
|
|
893
|
+
console.log(`${filename} already exists, skipping`)
|
|
894
|
+
} else {
|
|
895
|
+
fs.writeFileSync(dest, content)
|
|
896
|
+
console.log(`Created ${filename}`)
|
|
897
|
+
created++
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
if (created === 0) {
|
|
901
|
+
console.log('All files already exist, nothing to do')
|
|
902
|
+
}
|
|
903
|
+
return
|
|
904
|
+
}
|
|
905
|
+
|
|
643
906
|
if (argv.help || argv.h) {
|
|
644
907
|
console.log(`
|
|
645
|
-
Usage: jsee [schema.json] [options]
|
|
908
|
+
Usage: jsee [schema.json] [data...] [options]
|
|
909
|
+
jsee init [template] [--html]
|
|
910
|
+
|
|
911
|
+
Commands:
|
|
912
|
+
init [template] Scaffold a new JSEE project (templates: minimal, chat)
|
|
913
|
+
--html Generate a single index.html instead of separate files
|
|
646
914
|
|
|
647
915
|
Options:
|
|
648
916
|
-i, --inputs <file> Input schema file (default: schema.json)
|
|
@@ -651,24 +919,45 @@ Options:
|
|
|
651
919
|
-p, --port <number> Dev server port (default: 3000)
|
|
652
920
|
-v, --version <version> JSEE runtime version (default: latest)
|
|
653
921
|
-f, --fetch Fetch and bundle runtime + dependencies into output
|
|
654
|
-
-e, --execute Execute model server-side
|
|
922
|
+
-e, --execute Execute model server-side (auto-enabled when serving local .js models)
|
|
923
|
+
--client Force client-side execution (disable auto server-side)
|
|
655
924
|
-c, --cdn <url|bool> Rewrite model URLs for CDN deployment
|
|
656
925
|
-r, --runtime <mode> Runtime: auto|local|cdn|inline or a custom URL/path (default: auto)
|
|
657
926
|
--verbose Enable verbose logging
|
|
658
927
|
|
|
928
|
+
Data inputs:
|
|
929
|
+
Positional args after the schema file are mapped to schema inputs in order.
|
|
930
|
+
Named args (--name=value) are matched to schema inputs by name.
|
|
931
|
+
Values are auto-detected: numbers, JSON arrays/objects, file paths, or strings.
|
|
932
|
+
Inputs set from the CLI are locked (non-editable) in the GUI.
|
|
933
|
+
|
|
659
934
|
Examples:
|
|
935
|
+
jsee init Scaffold minimal project (schema.json + model.js)
|
|
936
|
+
jsee init chat Scaffold chat project
|
|
937
|
+
jsee init --html Generate single index.html
|
|
660
938
|
jsee schema.json Start dev server with schema
|
|
939
|
+
jsee schema.json 42 hello Pass positional data to first two inputs
|
|
940
|
+
jsee schema.json --a=100 --b=200 Pass named data inputs
|
|
941
|
+
jsee schema.json data.csv Pass a file path as input
|
|
661
942
|
jsee schema.json -o app.html Generate static HTML file
|
|
662
943
|
jsee schema.json -o app.html -f Generate self-contained HTML with bundled runtime
|
|
663
944
|
jsee -p 8080 Start dev server on port 8080
|
|
945
|
+
jsee report.pdf Serve a PDF file (auto-detected viewer)
|
|
946
|
+
jsee data/ Serve a folder (file browser with preview)
|
|
664
947
|
|
|
665
948
|
Documentation: https://jsee.org
|
|
666
949
|
`.trim())
|
|
667
950
|
return
|
|
668
951
|
}
|
|
669
952
|
|
|
670
|
-
//
|
|
671
|
-
|
|
953
|
+
// Check if first positional arg is a file or directory for identity serving
|
|
954
|
+
let identityResult = null
|
|
955
|
+
if (!imported && argv._.length > 0) {
|
|
956
|
+
identityResult = generateIdentitySchema(argv._[0], process.cwd())
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Set argv.inputs to the first positional argument if it looks like a schema/script
|
|
960
|
+
if (!identityResult && !imported && argv._.length > 0 && argv.inputs === argvDefault.inputs) {
|
|
672
961
|
argv.inputs = argv._[0]
|
|
673
962
|
}
|
|
674
963
|
|
|
@@ -709,7 +998,20 @@ Documentation: https://jsee.org
|
|
|
709
998
|
outputs = outputs.split(',')
|
|
710
999
|
}
|
|
711
1000
|
|
|
712
|
-
if (
|
|
1001
|
+
if (identityResult) {
|
|
1002
|
+
schema = identityResult.schema
|
|
1003
|
+
schemaPath = null
|
|
1004
|
+
log('Identity mode:', identityResult.serveFile ? 'file' : 'folder')
|
|
1005
|
+
if (identityResult.serveFile) {
|
|
1006
|
+
const outputType = schema.outputs[0].type
|
|
1007
|
+
const textTypes = ['table', 'markdown', 'html', 'object', 'code']
|
|
1008
|
+
if (textTypes.includes(outputType)) {
|
|
1009
|
+
schema.outputs[0].value = fs.readFileSync(identityResult.serveFile.resolved, 'utf8')
|
|
1010
|
+
} else {
|
|
1011
|
+
schema.outputs[0].value = identityResult.serveFile.path
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
} else if (inputs.length === 0) {
|
|
713
1015
|
console.error('No inputs provided')
|
|
714
1016
|
process.exit(1)
|
|
715
1017
|
} else if ((inputs.length === 1) && (inputs[0].includes('.json'))) {
|
|
@@ -722,7 +1024,7 @@ Documentation: https://jsee.org
|
|
|
722
1024
|
} else {
|
|
723
1025
|
// Array of js files
|
|
724
1026
|
// Generate schema
|
|
725
|
-
let jsdocData =
|
|
1027
|
+
let jsdocData = getJsdocToMarkdown().getTemplateDataSync({ files: inputs.map(f => path.join(cwd, f)) })
|
|
726
1028
|
schema = genSchema(jsdocData)
|
|
727
1029
|
// jsdocMarkdown = jsdoc2md.renderSync({
|
|
728
1030
|
// data: jsdocData,
|
|
@@ -740,10 +1042,15 @@ Documentation: https://jsee.org
|
|
|
740
1042
|
if (inp.alias) {
|
|
741
1043
|
argvAlias[inputName] = inp.alias
|
|
742
1044
|
}
|
|
743
|
-
// Use positional arguments as schema
|
|
1045
|
+
// Use positional arguments as schema input defaults
|
|
744
1046
|
if (imported && argv._.length > inp_index) {
|
|
745
1047
|
log('Using positional argument for input:', inputName, argv._[inp_index])
|
|
746
|
-
argvDefault[inputName] = argv._[inp_index]
|
|
1048
|
+
argvDefault[inputName] = detectArgValue(argv._[inp_index])
|
|
1049
|
+
} else if (!imported && argv._.length > inp_index + 1) {
|
|
1050
|
+
// Skip first positional arg (schema file), map rest to inputs
|
|
1051
|
+
const val = argv._[inp_index + 1]
|
|
1052
|
+
log('Using positional argument for input:', inputName, val)
|
|
1053
|
+
argvDefault[inputName] = detectArgValue(val)
|
|
747
1054
|
}
|
|
748
1055
|
// We don't need to duplicate defaults here, as we handle them on the frontend
|
|
749
1056
|
// else if (inp.default) {
|
|
@@ -786,19 +1093,49 @@ Documentation: https://jsee.org
|
|
|
786
1093
|
schema.model = [schema.model]
|
|
787
1094
|
}
|
|
788
1095
|
|
|
1096
|
+
// Resolve server-side execution mode
|
|
1097
|
+
const hasOutputs = Array.isArray(outputs) ? outputs.length > 0 : Boolean(outputs)
|
|
1098
|
+
let shouldExecute = argv.execute
|
|
1099
|
+
if (shouldExecute === 'auto') {
|
|
1100
|
+
const isServing = !hasOutputs
|
|
1101
|
+
if (argv.client) {
|
|
1102
|
+
shouldExecute = false
|
|
1103
|
+
} else if (isServing) {
|
|
1104
|
+
shouldExecute = schema.model.every(m =>
|
|
1105
|
+
m.url && m.url.endsWith('.js') && !isHttpUrl(m.url))
|
|
1106
|
+
} else {
|
|
1107
|
+
shouldExecute = false
|
|
1108
|
+
}
|
|
1109
|
+
} else {
|
|
1110
|
+
shouldExecute = Boolean(shouldExecute)
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Store original models before execute/CDN mutations (for client-side toggle)
|
|
1114
|
+
const originalModels = JSON.parse(JSON.stringify(schema.model))
|
|
1115
|
+
|
|
789
1116
|
// Server-side execution
|
|
790
|
-
//
|
|
1117
|
+
// Prepare the model functions to run on the server side
|
|
791
1118
|
// Schema model will be updated with the server url and POST method
|
|
792
|
-
if (
|
|
1119
|
+
if (shouldExecute) {
|
|
793
1120
|
await Promise.all(schema.model.map(async m => {
|
|
794
1121
|
log('Preparing a model to run on the server side:', m.name, m.url)
|
|
795
|
-
const
|
|
1122
|
+
const modelPath = path.join(schemaPath ? path.dirname(schemaPath) : cwd, m.url)
|
|
1123
|
+
let target = require(modelPath)
|
|
1124
|
+
// Handle browser-style model files (no module.exports) — eval the source
|
|
1125
|
+
// to extract the named function, similar to how the worker does it
|
|
1126
|
+
if (typeof target !== 'function' && (!target || Object.keys(target).length === 0)) {
|
|
1127
|
+
const src = fs.readFileSync(modelPath, 'utf-8')
|
|
1128
|
+
const fn = new Function(src + `\nreturn typeof ${m.name} === 'function' ? ${m.name} : undefined`)()
|
|
1129
|
+
if (typeof fn === 'function') {
|
|
1130
|
+
target = fn
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
796
1133
|
modelFuncs[m.name] = await getModelFuncJS(m, target, {log})
|
|
797
1134
|
m.type = 'post'
|
|
798
1135
|
m.url = `/${m.name}`
|
|
799
1136
|
m.worker = false
|
|
800
1137
|
}))
|
|
801
|
-
}
|
|
1138
|
+
}
|
|
802
1139
|
|
|
803
1140
|
// Switch to CDN for model files
|
|
804
1141
|
if (argv.cdn) {
|
|
@@ -838,7 +1175,7 @@ Documentation: https://jsee.org
|
|
|
838
1175
|
// Generate description block
|
|
839
1176
|
if (description) {
|
|
840
1177
|
const descriptionMd = fs.readFileSync(path.join(cwd, description), 'utf8')
|
|
841
|
-
descriptionHtml =
|
|
1178
|
+
descriptionHtml = getMarkdownConverter().render(descriptionMd)
|
|
842
1179
|
|
|
843
1180
|
if (descriptionMd.includes('---')) {
|
|
844
1181
|
descriptionTxt = descriptionMd
|
|
@@ -855,11 +1192,10 @@ Documentation: https://jsee.org
|
|
|
855
1192
|
// Generate jsee code
|
|
856
1193
|
let jseeHtml = ''
|
|
857
1194
|
let hiddenElementHtml = ''
|
|
858
|
-
const hasOutputs = Array.isArray(outputs) ? outputs.length > 0 : Boolean(outputs)
|
|
859
1195
|
const runtimeMode = resolveRuntimeMode(argv.runtime, argv.fetch, hasOutputs)
|
|
860
1196
|
if (argv.fetch) {
|
|
861
1197
|
// Fetch jsee code from the CDN or local server
|
|
862
|
-
const jseeCode = await loadRuntimeCode(argv.version)
|
|
1198
|
+
const jseeCode = await loadRuntimeCode(argv.version, schema)
|
|
863
1199
|
jseeHtml = `<script>${jseeCode}</script>`
|
|
864
1200
|
// Fetch model files and store them in hidden elements
|
|
865
1201
|
hiddenElementHtml += '<div id="hidden-storage" style="display: none;">'
|
|
@@ -933,14 +1269,14 @@ Documentation: https://jsee.org
|
|
|
933
1269
|
hiddenElementHtml += '</div>'
|
|
934
1270
|
} else {
|
|
935
1271
|
if (runtimeMode === 'inline') {
|
|
936
|
-
const jseeCode = await loadRuntimeCode(argv.version)
|
|
1272
|
+
const jseeCode = await loadRuntimeCode(argv.version, schema)
|
|
937
1273
|
jseeHtml = `<script>${jseeCode}</script>`
|
|
938
1274
|
} else if (runtimeMode === 'cdn') {
|
|
939
|
-
|
|
1275
|
+
const bundle = getBundleFilename(schema)
|
|
1276
|
+
jseeHtml = `<script src="https://cdn.jsdelivr.net/npm/@jseeio/jsee@${argv.version}/dist/${bundle}"></script>`
|
|
940
1277
|
} else if (runtimeMode === 'local') {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
: `<script src="http://localhost:${argv.port}/dist/jsee.runtime.js"></script>`
|
|
1278
|
+
const bundle = getBundleFilename(schema)
|
|
1279
|
+
jseeHtml = `<script src="http://localhost:${argv.port}/dist/${bundle}"></script>`
|
|
944
1280
|
} else {
|
|
945
1281
|
// Custom path/URL passed via --runtime (e.g. ./node_modules/.../jsee.js)
|
|
946
1282
|
jseeHtml = `<script src="${runtimeMode}"></script>`
|
|
@@ -1004,6 +1340,29 @@ Documentation: https://jsee.org
|
|
|
1004
1340
|
|
|
1005
1341
|
}
|
|
1006
1342
|
|
|
1343
|
+
// Build serve bar (only when serving, not for -o output)
|
|
1344
|
+
let serveBarHtml = ''
|
|
1345
|
+
let schemaScript = ''
|
|
1346
|
+
const isServing = !hasOutputs
|
|
1347
|
+
if (isServing) {
|
|
1348
|
+
const toggleHtml = shouldExecute
|
|
1349
|
+
? '<label style="cursor:pointer"><input type="checkbox" id="exec-toggle" style="margin-right:4px">Browser</label>'
|
|
1350
|
+
: ''
|
|
1351
|
+
serveBarHtml = `<div id="jsee-serve-bar" style="background:#f8f8f8;border-bottom:1px solid #e0e0e0;padding:6px 15px;font-size:13px;color:#828282;display:flex;align-items:center;gap:16px">
|
|
1352
|
+
<span style="font-family:monospace">localhost:${argv.port}</span>
|
|
1353
|
+
<span style="flex:1"></span>
|
|
1354
|
+
${toggleHtml}
|
|
1355
|
+
<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>
|
|
1356
|
+
</div>`
|
|
1357
|
+
}
|
|
1358
|
+
if (shouldExecute) {
|
|
1359
|
+
const clientSchema = JSON.parse(JSON.stringify(schema))
|
|
1360
|
+
clientSchema.model = originalModels
|
|
1361
|
+
schemaScript = `var schemaServer = ${JSON.stringify(schema, null, 2)}\n var schemaClient = ${JSON.stringify(clientSchema, null, 2)}`
|
|
1362
|
+
} else {
|
|
1363
|
+
schemaScript = `var schemaServer = ${JSON.stringify(schema, null, 2)}\n var schemaClient = null`
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1007
1366
|
const html = template(schema, {
|
|
1008
1367
|
descriptionHtml: pad(descriptionHtml, 8, 1),
|
|
1009
1368
|
descriptionTxt: descriptionTxt,
|
|
@@ -1012,6 +1371,8 @@ Documentation: https://jsee.org
|
|
|
1012
1371
|
hiddenElementHtml: hiddenElementHtml,
|
|
1013
1372
|
socialHtml: pad(socialHtml, 2, 1),
|
|
1014
1373
|
orgHtml: pad(orgHtml, 2, 1),
|
|
1374
|
+
serveBarHtml: serveBarHtml,
|
|
1375
|
+
schemaScript: schemaScript,
|
|
1015
1376
|
})
|
|
1016
1377
|
|
|
1017
1378
|
if (returnHtml) {
|
|
@@ -1037,30 +1398,78 @@ Documentation: https://jsee.org
|
|
|
1037
1398
|
const express = require('express')
|
|
1038
1399
|
const app = express()
|
|
1039
1400
|
app.use(express.json())
|
|
1040
|
-
|
|
1401
|
+
app.use(express.raw({ type: 'multipart/form-data', limit: '50mb' }))
|
|
1402
|
+
// CORS
|
|
1403
|
+
app.use((req, res, next) => {
|
|
1404
|
+
res.header('Access-Control-Allow-Origin', '*')
|
|
1405
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
|
1406
|
+
res.header('Access-Control-Allow-Headers', 'Content-Type')
|
|
1407
|
+
if (req.method === 'OPTIONS') return res.sendStatus(204)
|
|
1408
|
+
next()
|
|
1409
|
+
})
|
|
1410
|
+
// API discovery
|
|
1411
|
+
app.get('/api', (req, res) => {
|
|
1412
|
+
const models = schema.model.map(m => ({
|
|
1413
|
+
name: m.name,
|
|
1414
|
+
endpoint: m.url || '/' + m.name,
|
|
1415
|
+
method: 'POST'
|
|
1416
|
+
}))
|
|
1417
|
+
res.json({ schema, models })
|
|
1418
|
+
})
|
|
1419
|
+
// OpenAPI spec
|
|
1420
|
+
app.get('/api/openapi.json', (req, res) => {
|
|
1421
|
+
res.json(generateOpenAPISpec(schema))
|
|
1422
|
+
})
|
|
1423
|
+
if (shouldExecute) {
|
|
1041
1424
|
// Create post endpoint for executing the model
|
|
1042
1425
|
schema.model.forEach(m => {
|
|
1043
|
-
app.post(m.url, (req, res) => {
|
|
1426
|
+
app.post(m.url, async (req, res) => {
|
|
1044
1427
|
log(`Executing model: ${m.name}`)
|
|
1045
1428
|
if (m.name in modelFuncs) {
|
|
1046
1429
|
const modelFunc = modelFuncs[m.name]
|
|
1047
1430
|
try {
|
|
1048
1431
|
const dataFromArgv = getDataFromArgv(schema, argv)
|
|
1049
|
-
const
|
|
1432
|
+
const contentType = req.headers['content-type'] || ''
|
|
1433
|
+
let dataFromGUI = req.body
|
|
1434
|
+
if (contentType.includes('multipart/form-data') && Buffer.isBuffer(req.body)) {
|
|
1435
|
+
dataFromGUI = parseMultipart(contentType, req.body)
|
|
1436
|
+
}
|
|
1050
1437
|
const data = { ...dataFromGUI, ...dataFromArgv }
|
|
1051
1438
|
log('Data for model execution:', data)
|
|
1052
|
-
const result = modelFunc(data)
|
|
1053
|
-
res.json(result)
|
|
1439
|
+
const result = await modelFunc(data)
|
|
1440
|
+
res.json(serializeResult(result))
|
|
1054
1441
|
log(`Model ${m.name} executed successfully: `, result)
|
|
1055
1442
|
} catch (error) {
|
|
1056
1443
|
console.error('Error executing model:', error)
|
|
1057
1444
|
res.status(500).json({ error: error.message })
|
|
1058
1445
|
}
|
|
1446
|
+
} else {
|
|
1447
|
+
res.status(404).json({ error: 'Unknown model: ' + m.name })
|
|
1059
1448
|
}
|
|
1060
1449
|
})
|
|
1061
1450
|
log('Model execution endpoints created:', m.url)
|
|
1062
1451
|
})
|
|
1063
1452
|
}
|
|
1453
|
+
// Serve folder file listing as JSON
|
|
1454
|
+
app.get('/__jsee/folder', (req, res) => {
|
|
1455
|
+
const dirPath = req.query.path
|
|
1456
|
+
if (!dirPath) return res.status(400).json({ error: 'path required' })
|
|
1457
|
+
const resolved = path.resolve(schemaPath ? path.dirname(schemaPath) : cwd, dirPath)
|
|
1458
|
+
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {
|
|
1459
|
+
return res.status(404).json({ error: 'directory not found' })
|
|
1460
|
+
}
|
|
1461
|
+
res.json(readDirListing(resolved, dirPath))
|
|
1462
|
+
})
|
|
1463
|
+
// Serve individual files for viewer output
|
|
1464
|
+
app.get('/__jsee/file', (req, res) => {
|
|
1465
|
+
const filePath = req.query.path
|
|
1466
|
+
if (!filePath) return res.status(400).json({ error: 'path required' })
|
|
1467
|
+
const resolved = path.resolve(schemaPath ? path.dirname(schemaPath) : cwd, filePath)
|
|
1468
|
+
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) {
|
|
1469
|
+
return res.status(404).json({ error: 'file not found' })
|
|
1470
|
+
}
|
|
1471
|
+
res.sendFile(resolved)
|
|
1472
|
+
})
|
|
1064
1473
|
app.get('/', async (req, res) => {
|
|
1065
1474
|
log('Serving index.html')
|
|
1066
1475
|
res.send(await gen(pargv, true))
|
|
@@ -1089,3 +1498,4 @@ module.exports.resolveLocalImportFile = resolveLocalImportFile
|
|
|
1089
1498
|
module.exports.resolveFetchImport = resolveFetchImport
|
|
1090
1499
|
module.exports.resolveRuntimeMode = resolveRuntimeMode
|
|
1091
1500
|
module.exports.resolveOutputPath = resolveOutputPath
|
|
1501
|
+
module.exports.needsFullBundle = needsFullBundle
|