@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/LICENSE +21 -0
  3. package/README.md +583 -55
  4. package/dist/2b3e1faf89f94a483539.png +0 -0
  5. package/dist/416d91365b44e4b4f477.png +0 -0
  6. package/dist/8f2c4d11474275fbc161.png +0 -0
  7. package/dist/jsee.core.js +1 -0
  8. package/dist/jsee.full.js +1 -0
  9. package/dist/jsee.runtime.js +2 -1
  10. package/package.json +84 -18
  11. package/src/app.js +130 -33
  12. package/src/cli.js +474 -64
  13. package/src/extended-imports.js +11 -0
  14. package/src/main.js +264 -45
  15. package/src/overlay.js +26 -1
  16. package/src/utils.js +390 -12
  17. package/templates/common-inputs.js +88 -0
  18. package/templates/common-outputs.js +340 -4
  19. package/templates/minimal-app.vue +367 -0
  20. package/templates/minimal-input.vue +573 -0
  21. package/templates/minimal-output.vue +426 -0
  22. package/templates/virtual-table.vue +194 -0
  23. package/.claude/settings.local.json +0 -13
  24. package/.eslintrc.js +0 -38
  25. package/AGENTS.md +0 -65
  26. package/CLAUDE.md +0 -5
  27. package/CNAME +0 -1
  28. package/_config.yml +0 -26
  29. package/dist/jsee.js +0 -1
  30. package/jest-puppeteer.config.js +0 -14
  31. package/jest.config.js +0 -8
  32. package/jest.unit.config.js +0 -8
  33. package/load/index.html +0 -52
  34. package/templates/bulma-app.vue +0 -242
  35. package/templates/bulma-input.vue +0 -125
  36. package/templates/bulma-output.vue +0 -101
  37. package/test/class.html +0 -22
  38. package/test/code.html +0 -25
  39. package/test/codew.html +0 -25
  40. package/test/example-class.js +0 -8
  41. package/test/example-sum.js +0 -3
  42. package/test/fixtures/lodash-like.js +0 -15
  43. package/test/fixtures/upload-sample.csv +0 -3
  44. package/test/importw.html +0 -28
  45. package/test/minimal.html +0 -14
  46. package/test/minimal1.html +0 -13
  47. package/test/minimal2.html +0 -15
  48. package/test/minimal3.html +0 -10
  49. package/test/minimal4.html +0 -22
  50. package/test/pipeline.html +0 -52
  51. package/test/python.html +0 -41
  52. package/test/string.html +0 -26
  53. package/test/stringw.html +0 -29
  54. package/test/sum.schema.json +0 -17
  55. package/test/sumw.schema.json +0 -15
  56. package/test/test-basic.test.js +0 -603
  57. package/test/test-python.test.js +0 -23
  58. package/test/unit/cli-fetch.test.js +0 -229
  59. package/test/unit/utils.test.js +0 -888
  60. 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
- const jsdoc2md = require('jsdoc-to-markdown')
8
- const showdown = require('showdown')
9
- const showdownKatex = require('showdown-katex')
10
- const converter = new showdown.Converter({
11
- extensions: [
12
- showdownKatex({
13
- throwOnError: true,
14
- displayMode: true,
15
- errorColor: '#1500ff',
16
- output: 'mathml'
17
- }),
18
- ],
19
- tables: true
20
- })
21
- showdown.setFlavor('github')
22
-
23
- const { getModelFuncJS, sanitizeName } = require('./utils.js')
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
- async function loadRuntimeCode (version) {
170
- if (version === 'dev') {
171
- return fs.readFileSync(path.join(__dirname, '..', 'dist', 'jsee.js'), 'utf8')
172
- }
173
- if (version === 'latest') {
174
- return fs.readFileSync(path.join(__dirname, '..', 'dist', 'jsee.runtime.js'), 'utf8')
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/jsee.runtime.js`)
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
- #download-btn { float: right; margin-top: 10px; padding: 10px; background-color: white; border: none; cursor: pointer; }
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
- const schema = ${JSON.stringify(schema, null, 2)}
597
- const title = "${title}"
716
+ ${blocks.schemaScript}
717
+ var currentSchema = schemaServer || schemaClient
718
+ var container = document.getElementById('jsee-container')
598
719
  var env = new JSEE({
599
- container: document.getElementById('jsee-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: false, // execute the model code on the server
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
- // Set argv.inputs to the first non-option argument if it exists
671
- if (!imported && argv._.length > 0 && !argv.inputs) {
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 (inputs.length === 0) {
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 = jsdoc2md.getTemplateDataSync({ files: inputs.map(f => path.join(cwd, f)) })
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 inputs defaults if JSEE CLI is imported
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
- // If execute is true, we will prepare the model functions to run on the server side
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 (argv.execute) {
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 target = require(path.join(schemaPath ? path.dirname(schemaPath) : cwd, m.url))
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 = converter.makeHtml(descriptionMd)
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
- jseeHtml = `<script src="https://cdn.jsdelivr.net/npm/@jseeio/jsee@${argv.version}/dist/jsee.runtime.js"></script>`
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
- jseeHtml = argv.version === 'dev'
942
- ? `<script src="http://localhost:${argv.port}/dist/jsee.js"></script>`
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
- if (argv.execute) {
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 dataFromGUI = req.body
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