@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.
- package/CHANGELOG.md +96 -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 +127 -32
- package/src/browser-bundle-node.js +9 -0
- package/src/cli.js +479 -67
- package/src/extended-imports.js +11 -0
- package/src/main.js +232 -44
- package/src/overlay.js +26 -1
- package/src/utils.js +386 -16
- 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 -15
- 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/dump.sh +0 -23
- package/jest-puppeteer.config.js +0 -14
- package/jest.config.js +0 -8
- package/jest.unit.config.js +0 -8
- package/jsee.dump.txt +0 -5459
- 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/arrow-main.html +0 -18
- package/test/arrow-worker.html +0 -18
- 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/runtime-arrow.html +0 -18
- 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 -630
- package/test/test-python.test.js +0 -23
- package/test/unit/cli-fetch.test.js +0 -229
- package/test/unit/utils.test.js +0 -908
- 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>`
|
|
@@ -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
|
|
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:
|
|
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
|
-
//
|
|
671
|
-
|
|
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:',
|
|
686
|
-
log('Require file:',
|
|
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 (
|
|
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 =
|
|
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
|
|
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
|
-
//
|
|
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 (
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
942
|
-
|
|
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
|
-
|
|
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
|
|
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
|