@jseeio/jsee 0.4.2 → 0.8.1

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 (66) hide show
  1. package/CHANGELOG.md +96 -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 +127 -32
  12. package/src/browser-bundle-node.js +9 -0
  13. package/src/cli.js +479 -67
  14. package/src/extended-imports.js +11 -0
  15. package/src/main.js +232 -44
  16. package/src/overlay.js +26 -1
  17. package/src/utils.js +386 -16
  18. package/templates/common-inputs.js +88 -0
  19. package/templates/common-outputs.js +340 -4
  20. package/templates/minimal-app.vue +367 -0
  21. package/templates/minimal-input.vue +573 -0
  22. package/templates/minimal-output.vue +426 -0
  23. package/templates/virtual-table.vue +194 -0
  24. package/.claude/settings.local.json +0 -15
  25. package/.eslintrc.js +0 -38
  26. package/AGENTS.md +0 -65
  27. package/CLAUDE.md +0 -5
  28. package/CNAME +0 -1
  29. package/_config.yml +0 -26
  30. package/dist/jsee.js +0 -1
  31. package/dump.sh +0 -23
  32. package/jest-puppeteer.config.js +0 -14
  33. package/jest.config.js +0 -8
  34. package/jest.unit.config.js +0 -8
  35. package/jsee.dump.txt +0 -5459
  36. package/load/index.html +0 -52
  37. package/templates/bulma-app.vue +0 -242
  38. package/templates/bulma-input.vue +0 -125
  39. package/templates/bulma-output.vue +0 -101
  40. package/test/arrow-main.html +0 -18
  41. package/test/arrow-worker.html +0 -18
  42. package/test/class.html +0 -22
  43. package/test/code.html +0 -25
  44. package/test/codew.html +0 -25
  45. package/test/example-class.js +0 -8
  46. package/test/example-sum.js +0 -3
  47. package/test/fixtures/lodash-like.js +0 -15
  48. package/test/fixtures/upload-sample.csv +0 -3
  49. package/test/importw.html +0 -28
  50. package/test/minimal.html +0 -14
  51. package/test/minimal1.html +0 -13
  52. package/test/minimal2.html +0 -15
  53. package/test/minimal3.html +0 -10
  54. package/test/minimal4.html +0 -22
  55. package/test/pipeline.html +0 -52
  56. package/test/python.html +0 -41
  57. package/test/runtime-arrow.html +0 -18
  58. package/test/string.html +0 -26
  59. package/test/stringw.html +0 -29
  60. package/test/sum.schema.json +0 -17
  61. package/test/sumw.schema.json +0 -15
  62. package/test/test-basic.test.js +0 -630
  63. package/test/test-python.test.js +0 -23
  64. package/test/unit/cli-fetch.test.js +0 -229
  65. package/test/unit/utils.test.js +0 -908
  66. 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>`
@@ -609,7 +739,9 @@ function template(schema, blocks) {
609
739
 
610
740
  async function gen (pargv, returnHtml=false) {
611
741
  // Determine if JSEE CLI is imported or run directly
612
- const imported = path.dirname(__dirname) !== path.dirname(require.main.path)
742
+ const mainPath = require.main && require.main.path ? require.main.path : process.cwd()
743
+ const mainFilename = require.main && require.main.filename ? require.main.filename : ''
744
+ const imported = path.dirname(__dirname) !== path.dirname(mainPath)
613
745
 
614
746
  // First pass over CLI arguments
615
747
  // JSEE-level args
@@ -625,7 +757,7 @@ async function gen (pargv, returnHtml=false) {
625
757
  runtime: 'r',
626
758
  }
627
759
  const argvDefault = {
628
- execute: false, // execute the model code on the server
760
+ execute: 'auto', // execute the model code on the server (auto = server-side when serving local .js models)
629
761
  fetch: false, // fetch the JSEE runtime from the CDN or local server
630
762
  inputs: 'schema.json', // default input is schema.json in the current working directory
631
763
  port: 3000, // default port for the server
@@ -637,12 +769,150 @@ async function gen (pargv, returnHtml=false) {
637
769
  let argv = minimist(pargv, {
638
770
  alias: argvAlias,
639
771
  default: argvDefault,
640
- boolean: ['help', 'h'],
772
+ boolean: ['help', 'h', 'html', 'client'],
641
773
  })
642
774
 
775
+ // ── jsee init <template> ──────────────────────────────────────────
776
+ if (argv._[0] === 'init') {
777
+ const tpl = argv._[1] || 'minimal'
778
+ const cwd = process.cwd()
779
+
780
+ if (argv.html) {
781
+ // Single index.html with inline schema + CDN script
782
+ const dest = path.join(cwd, 'index.html')
783
+ if (fs.existsSync(dest)) {
784
+ console.log('index.html already exists, skipping')
785
+ return
786
+ }
787
+ let htmlContent
788
+ if (tpl === 'chat') {
789
+ htmlContent = `<!DOCTYPE html>
790
+ <html lang="en">
791
+ <head>
792
+ <meta charset="utf-8">
793
+ <meta name="viewport" content="width=device-width, initial-scale=1">
794
+ <title>Chat</title>
795
+ </head>
796
+ <body>
797
+ <div id="jsee-container"></div>
798
+ <script src="https://cdn.jsdelivr.net/npm/@jseeio/jsee@latest/dist/jsee.core.js"></script>
799
+ <script>
800
+ new JSEE({
801
+ container: document.getElementById('jsee-container'),
802
+ schema: {
803
+ model: { code: function chat(inputs) { return { chat: 'You said: ' + inputs.message } }, worker: false },
804
+ inputs: [{ name: 'message', type: 'string', enter: true }],
805
+ outputs: [{ name: 'chat', type: 'chat' }]
806
+ }
807
+ })
808
+ </script>
809
+ </body>
810
+ </html>`
811
+ } else {
812
+ htmlContent = `<!DOCTYPE html>
813
+ <html lang="en">
814
+ <head>
815
+ <meta charset="utf-8">
816
+ <meta name="viewport" content="width=device-width, initial-scale=1">
817
+ <title>My App</title>
818
+ </head>
819
+ <body>
820
+ <div id="jsee-container"></div>
821
+ <script src="https://cdn.jsdelivr.net/npm/@jseeio/jsee@latest/dist/jsee.core.js"></script>
822
+ <script>
823
+ new JSEE({
824
+ container: document.getElementById('jsee-container'),
825
+ schema: {
826
+ model: { code: function greet(inputs) { return { greeting: (inputs.name + '\\n').repeat(inputs.count) } }, worker: false },
827
+ inputs: [
828
+ { name: 'name', type: 'string', default: 'World' },
829
+ { name: 'count', type: 'slider', min: 1, max: 10, default: 3 }
830
+ ],
831
+ outputs: [{ name: 'greeting', type: 'code' }]
832
+ }
833
+ })
834
+ </script>
835
+ </body>
836
+ </html>`
837
+ }
838
+ fs.writeFileSync(dest, htmlContent)
839
+ console.log('Created index.html')
840
+ return
841
+ }
842
+
843
+ // Multi-file scaffolding
844
+ const files = {}
845
+ if (tpl === 'chat') {
846
+ files['schema.json'] = JSON.stringify({
847
+ model: { url: 'model.js', name: 'chat', worker: false },
848
+ inputs: [{ name: 'message', type: 'string', enter: true }],
849
+ outputs: [{ name: 'chat', type: 'chat' }]
850
+ }, null, 2) + '\n'
851
+ files['model.js'] = `function chat(inputs) {
852
+ return { chat: 'You said: ' + inputs.message }
853
+ }
854
+ `
855
+ files['README.md'] = `# Chat
856
+
857
+ A chat app built with [JSEE](https://jsee.org).
858
+
859
+ ## Run
860
+
861
+ \`\`\`bash
862
+ npx @jseeio/jsee schema.json
863
+ \`\`\`
864
+ `
865
+ } else {
866
+ // minimal (default)
867
+ files['schema.json'] = JSON.stringify({
868
+ model: { url: 'model.js', name: 'greet' },
869
+ inputs: [
870
+ { name: 'name', type: 'string', default: 'World' },
871
+ { name: 'count', type: 'slider', min: 1, max: 10, default: 3 }
872
+ ],
873
+ outputs: [{ name: 'greeting', type: 'code' }]
874
+ }, null, 2) + '\n'
875
+ files['model.js'] = `function greet(inputs) {
876
+ return { greeting: (inputs.name + '\\n').repeat(inputs.count) }
877
+ }
878
+ `
879
+ files['README.md'] = `# My App
880
+
881
+ A web app built with [JSEE](https://jsee.org).
882
+
883
+ ## Run
884
+
885
+ \`\`\`bash
886
+ npx @jseeio/jsee schema.json
887
+ \`\`\`
888
+ `
889
+ }
890
+
891
+ let created = 0
892
+ for (const [filename, content] of Object.entries(files)) {
893
+ const dest = path.join(cwd, filename)
894
+ if (fs.existsSync(dest)) {
895
+ console.log(`${filename} already exists, skipping`)
896
+ } else {
897
+ fs.writeFileSync(dest, content)
898
+ console.log(`Created ${filename}`)
899
+ created++
900
+ }
901
+ }
902
+ if (created === 0) {
903
+ console.log('All files already exist, nothing to do')
904
+ }
905
+ return
906
+ }
907
+
643
908
  if (argv.help || argv.h) {
644
909
  console.log(`
645
- Usage: jsee [schema.json] [options]
910
+ Usage: jsee [schema.json] [data...] [options]
911
+ jsee init [template] [--html]
912
+
913
+ Commands:
914
+ init [template] Scaffold a new JSEE project (templates: minimal, chat)
915
+ --html Generate a single index.html instead of separate files
646
916
 
647
917
  Options:
648
918
  -i, --inputs <file> Input schema file (default: schema.json)
@@ -651,24 +921,45 @@ Options:
651
921
  -p, --port <number> Dev server port (default: 3000)
652
922
  -v, --version <version> JSEE runtime version (default: latest)
653
923
  -f, --fetch Fetch and bundle runtime + dependencies into output
654
- -e, --execute Execute model server-side
924
+ -e, --execute Execute model server-side (auto-enabled when serving local .js models)
925
+ --client Force client-side execution (disable auto server-side)
655
926
  -c, --cdn <url|bool> Rewrite model URLs for CDN deployment
656
927
  -r, --runtime <mode> Runtime: auto|local|cdn|inline or a custom URL/path (default: auto)
657
928
  --verbose Enable verbose logging
658
929
 
930
+ Data inputs:
931
+ Positional args after the schema file are mapped to schema inputs in order.
932
+ Named args (--name=value) are matched to schema inputs by name.
933
+ Values are auto-detected: numbers, JSON arrays/objects, file paths, or strings.
934
+ Inputs set from the CLI are locked (non-editable) in the GUI.
935
+
659
936
  Examples:
937
+ jsee init Scaffold minimal project (schema.json + model.js)
938
+ jsee init chat Scaffold chat project
939
+ jsee init --html Generate single index.html
660
940
  jsee schema.json Start dev server with schema
941
+ jsee schema.json 42 hello Pass positional data to first two inputs
942
+ jsee schema.json --a=100 --b=200 Pass named data inputs
943
+ jsee schema.json data.csv Pass a file path as input
661
944
  jsee schema.json -o app.html Generate static HTML file
662
945
  jsee schema.json -o app.html -f Generate self-contained HTML with bundled runtime
663
946
  jsee -p 8080 Start dev server on port 8080
947
+ jsee report.pdf Serve a PDF file (auto-detected viewer)
948
+ jsee data/ Serve a folder (file browser with preview)
664
949
 
665
950
  Documentation: https://jsee.org
666
951
  `.trim())
667
952
  return
668
953
  }
669
954
 
670
- // Set argv.inputs to the first non-option argument if it exists
671
- if (!imported && argv._.length > 0 && !argv.inputs) {
955
+ // Check if first positional arg is a file or directory for identity serving
956
+ let identityResult = null
957
+ if (!imported && argv._.length > 0) {
958
+ identityResult = generateIdentitySchema(argv._[0], process.cwd())
959
+ }
960
+
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) {
672
963
  argv.inputs = argv._[0]
673
964
  }
674
965
 
@@ -682,8 +973,8 @@ Documentation: https://jsee.org
682
973
  log('Current working directory:', process.cwd())
683
974
  log('Script location:', __dirname)
684
975
  log('Script file:', __filename)
685
- log('Require location:', require.main.path)
686
- log('Require file:', require.main.filename)
976
+ log('Require location:', mainPath)
977
+ log('Require file:', mainFilename)
687
978
 
688
979
  let cwd = process.cwd()
689
980
  let inputs = argv.inputs
@@ -709,7 +1000,20 @@ Documentation: https://jsee.org
709
1000
  outputs = outputs.split(',')
710
1001
  }
711
1002
 
712
- if (inputs.length === 0) {
1003
+ if (identityResult) {
1004
+ schema = identityResult.schema
1005
+ schemaPath = null
1006
+ log('Identity mode:', identityResult.serveFile ? 'file' : 'folder')
1007
+ if (identityResult.serveFile) {
1008
+ const outputType = schema.outputs[0].type
1009
+ const textTypes = ['table', 'markdown', 'html', 'object', 'code']
1010
+ if (textTypes.includes(outputType)) {
1011
+ schema.outputs[0].value = fs.readFileSync(identityResult.serveFile.resolved, 'utf8')
1012
+ } else {
1013
+ schema.outputs[0].value = identityResult.serveFile.path
1014
+ }
1015
+ }
1016
+ } else if (inputs.length === 0) {
713
1017
  console.error('No inputs provided')
714
1018
  process.exit(1)
715
1019
  } else if ((inputs.length === 1) && (inputs[0].includes('.json'))) {
@@ -722,7 +1026,7 @@ Documentation: https://jsee.org
722
1026
  } else {
723
1027
  // Array of js files
724
1028
  // Generate schema
725
- let jsdocData = jsdoc2md.getTemplateDataSync({ files: inputs.map(f => path.join(cwd, f)) })
1029
+ let jsdocData = getJsdocToMarkdown().getTemplateDataSync({ files: inputs.map(f => path.join(cwd, f)) })
726
1030
  schema = genSchema(jsdocData)
727
1031
  // jsdocMarkdown = jsdoc2md.renderSync({
728
1032
  // data: jsdocData,
@@ -740,10 +1044,15 @@ Documentation: https://jsee.org
740
1044
  if (inp.alias) {
741
1045
  argvAlias[inputName] = inp.alias
742
1046
  }
743
- // Use positional arguments as schema inputs defaults if JSEE CLI is imported
1047
+ // Use positional arguments as schema input defaults
744
1048
  if (imported && argv._.length > inp_index) {
745
1049
  log('Using positional argument for input:', inputName, argv._[inp_index])
746
- argvDefault[inputName] = argv._[inp_index]
1050
+ argvDefault[inputName] = detectArgValue(argv._[inp_index])
1051
+ } else if (!imported && argv._.length > inp_index + 1) {
1052
+ // Skip first positional arg (schema file), map rest to inputs
1053
+ const val = argv._[inp_index + 1]
1054
+ log('Using positional argument for input:', inputName, val)
1055
+ argvDefault[inputName] = detectArgValue(val)
747
1056
  }
748
1057
  // We don't need to duplicate defaults here, as we handle them on the frontend
749
1058
  // else if (inp.default) {
@@ -786,19 +1095,49 @@ Documentation: https://jsee.org
786
1095
  schema.model = [schema.model]
787
1096
  }
788
1097
 
1098
+ // Resolve server-side execution mode
1099
+ const hasOutputs = Array.isArray(outputs) ? outputs.length > 0 : Boolean(outputs)
1100
+ let shouldExecute = argv.execute
1101
+ if (shouldExecute === 'auto') {
1102
+ const isServing = !hasOutputs
1103
+ if (argv.client) {
1104
+ shouldExecute = false
1105
+ } else if (isServing) {
1106
+ shouldExecute = schema.model.every(m =>
1107
+ m.url && m.url.endsWith('.js') && !isHttpUrl(m.url))
1108
+ } else {
1109
+ shouldExecute = false
1110
+ }
1111
+ } else {
1112
+ shouldExecute = Boolean(shouldExecute)
1113
+ }
1114
+
1115
+ // Store original models before execute/CDN mutations (for client-side toggle)
1116
+ const originalModels = JSON.parse(JSON.stringify(schema.model))
1117
+
789
1118
  // Server-side execution
790
- // If execute is true, we will prepare the model functions to run on the server side
1119
+ // Prepare the model functions to run on the server side
791
1120
  // Schema model will be updated with the server url and POST method
792
- if (argv.execute) {
1121
+ if (shouldExecute) {
793
1122
  await Promise.all(schema.model.map(async m => {
794
1123
  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))
1124
+ const modelPath = path.join(schemaPath ? path.dirname(schemaPath) : cwd, m.url)
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
+ }
796
1135
  modelFuncs[m.name] = await getModelFuncJS(m, target, {log})
797
1136
  m.type = 'post'
798
1137
  m.url = `/${m.name}`
799
1138
  m.worker = false
800
1139
  }))
801
- }
1140
+ }
802
1141
 
803
1142
  // Switch to CDN for model files
804
1143
  if (argv.cdn) {
@@ -838,7 +1177,7 @@ Documentation: https://jsee.org
838
1177
  // Generate description block
839
1178
  if (description) {
840
1179
  const descriptionMd = fs.readFileSync(path.join(cwd, description), 'utf8')
841
- descriptionHtml = converter.makeHtml(descriptionMd)
1180
+ descriptionHtml = getMarkdownConverter().render(descriptionMd)
842
1181
 
843
1182
  if (descriptionMd.includes('---')) {
844
1183
  descriptionTxt = descriptionMd
@@ -855,11 +1194,10 @@ Documentation: https://jsee.org
855
1194
  // Generate jsee code
856
1195
  let jseeHtml = ''
857
1196
  let hiddenElementHtml = ''
858
- const hasOutputs = Array.isArray(outputs) ? outputs.length > 0 : Boolean(outputs)
859
1197
  const runtimeMode = resolveRuntimeMode(argv.runtime, argv.fetch, hasOutputs)
860
1198
  if (argv.fetch) {
861
1199
  // Fetch jsee code from the CDN or local server
862
- const jseeCode = await loadRuntimeCode(argv.version)
1200
+ const jseeCode = await loadRuntimeCode(argv.version, schema)
863
1201
  jseeHtml = `<script>${jseeCode}</script>`
864
1202
  // Fetch model files and store them in hidden elements
865
1203
  hiddenElementHtml += '<div id="hidden-storage" style="display: none;">'
@@ -933,14 +1271,14 @@ Documentation: https://jsee.org
933
1271
  hiddenElementHtml += '</div>'
934
1272
  } else {
935
1273
  if (runtimeMode === 'inline') {
936
- const jseeCode = await loadRuntimeCode(argv.version)
1274
+ const jseeCode = await loadRuntimeCode(argv.version, schema)
937
1275
  jseeHtml = `<script>${jseeCode}</script>`
938
1276
  } else if (runtimeMode === 'cdn') {
939
- jseeHtml = `<script src="https://cdn.jsdelivr.net/npm/@jseeio/jsee@${argv.version}/dist/jsee.runtime.js"></script>`
1277
+ const bundle = getBundleFilename(schema)
1278
+ jseeHtml = `<script src="https://cdn.jsdelivr.net/npm/@jseeio/jsee@${argv.version}/dist/${bundle}"></script>`
940
1279
  } 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>`
1280
+ const bundle = getBundleFilename(schema)
1281
+ jseeHtml = `<script src="http://localhost:${argv.port}/dist/${bundle}"></script>`
944
1282
  } else {
945
1283
  // Custom path/URL passed via --runtime (e.g. ./node_modules/.../jsee.js)
946
1284
  jseeHtml = `<script src="${runtimeMode}"></script>`
@@ -1004,6 +1342,29 @@ Documentation: https://jsee.org
1004
1342
 
1005
1343
  }
1006
1344
 
1345
+ // Build serve bar (only when serving, not for -o output)
1346
+ let serveBarHtml = ''
1347
+ let schemaScript = ''
1348
+ const isServing = !hasOutputs
1349
+ if (isServing) {
1350
+ const toggleHtml = shouldExecute
1351
+ ? '<label style="cursor:pointer"><input type="checkbox" id="exec-toggle" style="margin-right:4px">Browser</label>'
1352
+ : ''
1353
+ 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">
1354
+ <span style="font-family:monospace">localhost:${argv.port}</span>
1355
+ <span style="flex:1"></span>
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>`
1359
+ }
1360
+ if (shouldExecute) {
1361
+ const clientSchema = JSON.parse(JSON.stringify(schema))
1362
+ clientSchema.model = originalModels
1363
+ schemaScript = `var schemaServer = ${JSON.stringify(schema, null, 2)}\n var schemaClient = ${JSON.stringify(clientSchema, null, 2)}`
1364
+ } else {
1365
+ schemaScript = `var schemaServer = ${JSON.stringify(schema, null, 2)}\n var schemaClient = null`
1366
+ }
1367
+
1007
1368
  const html = template(schema, {
1008
1369
  descriptionHtml: pad(descriptionHtml, 8, 1),
1009
1370
  descriptionTxt: descriptionTxt,
@@ -1012,6 +1373,8 @@ Documentation: https://jsee.org
1012
1373
  hiddenElementHtml: hiddenElementHtml,
1013
1374
  socialHtml: pad(socialHtml, 2, 1),
1014
1375
  orgHtml: pad(orgHtml, 2, 1),
1376
+ serveBarHtml: serveBarHtml,
1377
+ schemaScript: schemaScript,
1015
1378
  })
1016
1379
 
1017
1380
  if (returnHtml) {
@@ -1037,30 +1400,78 @@ Documentation: https://jsee.org
1037
1400
  const express = require('express')
1038
1401
  const app = express()
1039
1402
  app.use(express.json())
1040
- if (argv.execute) {
1403
+ app.use(express.raw({ type: 'multipart/form-data', limit: '50mb' }))
1404
+ // CORS
1405
+ app.use((req, res, next) => {
1406
+ res.header('Access-Control-Allow-Origin', '*')
1407
+ res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
1408
+ res.header('Access-Control-Allow-Headers', 'Content-Type')
1409
+ if (req.method === 'OPTIONS') return res.sendStatus(204)
1410
+ next()
1411
+ })
1412
+ // API discovery
1413
+ app.get('/api', (req, res) => {
1414
+ const models = schema.model.map(m => ({
1415
+ name: m.name,
1416
+ endpoint: m.url || '/' + m.name,
1417
+ method: 'POST'
1418
+ }))
1419
+ res.json({ schema, models })
1420
+ })
1421
+ // OpenAPI spec
1422
+ app.get('/api/openapi.json', (req, res) => {
1423
+ res.json(generateOpenAPISpec(schema))
1424
+ })
1425
+ if (shouldExecute) {
1041
1426
  // Create post endpoint for executing the model
1042
1427
  schema.model.forEach(m => {
1043
- app.post(m.url, (req, res) => {
1428
+ app.post(m.url, async (req, res) => {
1044
1429
  log(`Executing model: ${m.name}`)
1045
1430
  if (m.name in modelFuncs) {
1046
1431
  const modelFunc = modelFuncs[m.name]
1047
1432
  try {
1048
1433
  const dataFromArgv = getDataFromArgv(schema, argv)
1049
- const dataFromGUI = req.body
1434
+ const contentType = req.headers['content-type'] || ''
1435
+ let dataFromGUI = req.body
1436
+ if (contentType.includes('multipart/form-data') && Buffer.isBuffer(req.body)) {
1437
+ dataFromGUI = parseMultipart(contentType, req.body)
1438
+ }
1050
1439
  const data = { ...dataFromGUI, ...dataFromArgv }
1051
1440
  log('Data for model execution:', data)
1052
- const result = modelFunc(data)
1053
- res.json(result)
1441
+ const result = await modelFunc(data)
1442
+ res.json(serializeResult(result))
1054
1443
  log(`Model ${m.name} executed successfully: `, result)
1055
1444
  } catch (error) {
1056
1445
  console.error('Error executing model:', error)
1057
1446
  res.status(500).json({ error: error.message })
1058
1447
  }
1448
+ } else {
1449
+ res.status(404).json({ error: 'Unknown model: ' + m.name })
1059
1450
  }
1060
1451
  })
1061
1452
  log('Model execution endpoints created:', m.url)
1062
1453
  })
1063
1454
  }
1455
+ // Serve folder file listing as JSON
1456
+ app.get('/__jsee/folder', (req, res) => {
1457
+ const dirPath = req.query.path
1458
+ if (!dirPath) return res.status(400).json({ error: 'path required' })
1459
+ const resolved = path.resolve(schemaPath ? path.dirname(schemaPath) : cwd, dirPath)
1460
+ if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {
1461
+ return res.status(404).json({ error: 'directory not found' })
1462
+ }
1463
+ res.json(readDirListing(resolved, dirPath))
1464
+ })
1465
+ // Serve individual files for viewer output
1466
+ app.get('/__jsee/file', (req, res) => {
1467
+ const filePath = req.query.path
1468
+ if (!filePath) return res.status(400).json({ error: 'path required' })
1469
+ const resolved = path.resolve(schemaPath ? path.dirname(schemaPath) : cwd, filePath)
1470
+ if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) {
1471
+ return res.status(404).json({ error: 'file not found' })
1472
+ }
1473
+ res.sendFile(resolved)
1474
+ })
1064
1475
  app.get('/', async (req, res) => {
1065
1476
  log('Serving index.html')
1066
1477
  res.send(await gen(pargv, true))
@@ -1089,3 +1500,4 @@ module.exports.resolveLocalImportFile = resolveLocalImportFile
1089
1500
  module.exports.resolveFetchImport = resolveFetchImport
1090
1501
  module.exports.resolveRuntimeMode = resolveRuntimeMode
1091
1502
  module.exports.resolveOutputPath = resolveOutputPath
1503
+ module.exports.needsFullBundle = needsFullBundle