@jseeio/jsee 0.3.7 → 0.3.8

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/src/cli.js CHANGED
@@ -20,6 +20,8 @@ const converter = new showdown.Converter({
20
20
  })
21
21
  showdown.setFlavor('github')
22
22
 
23
+ const { getModelFuncJS, sanitizeName } = require('./utils.js')
24
+
23
25
  // left padding of multiple lines
24
26
  function pad (str, len, start=0) {
25
27
  return str.split('\n').map((s, i) => i >= start ? ' '.repeat(len) + s : s).join('\n')
@@ -29,585 +31,261 @@ function depad (str, len) {
29
31
  return str.split('\n').map(s => s.slice(len)).join('\n')
30
32
  }
31
33
 
32
- // Adding async here breaks express. TODO: investigate
33
- async function gen (pargv, returnHtml=false) {
34
- pargv = pargv.slice(2) // Remove the first two elements (node and script path)
35
-
36
- // print all folders (file location, cwd, etc.)
37
- console.log('Current working directory:', process.cwd())
38
- console.log('Script location:', __dirname)
39
- console.log('Script file:', __filename)
40
- console.log('Require', require.main.filename)
41
-
42
- const argvAlias ={
43
- inputs: 'i',
44
- outputs: 'o',
45
- description: 'd',
46
- ga: 'g',
47
- port: 'p',
48
- version: 'v',
49
- fetch: 'f',
50
- execute: 'e'
51
- }
52
- const argvDefault = {
53
- execute: false, // execute the model code on the server
54
- fetch: false,
55
- inputs: '',
56
- port: 3000,
57
- version: 'latest'
34
+ function getDataFromArgv (schema, argv, loadFiles=true) {
35
+ let data = {}
36
+ if (schema.inputs) {
37
+ schema.inputs.forEach(inp => {
38
+ const inputName = sanitizeName(inp.name)
39
+ console.log('Processing input:', inp.name, 'as', inputName)
40
+ if (inputName in argv) {
41
+ switch (inp.type) {
42
+ case 'file':
43
+ if (!loadFiles) {
44
+ // If we don't want to load files, just set the value to the file path
45
+ data[inp.name] = argv[inputName]
46
+ break
47
+ } else if (fs.existsSync(argv[inputName])) {
48
+ data[inp.name] = fs.readFileSync(argv[inputName], 'utf8')
49
+ } else {
50
+ console.error(`File not found: ${argv[inputName]}`)
51
+ process.exit(1)
52
+ }
53
+ break
54
+ case 'int':
55
+ data[inp.name] = parseInt(argv[inputName], 10)
56
+ break
57
+ case 'float':
58
+ data[inp.name] = parseFloat(argv[inputName])
59
+ break
60
+ case 'string':
61
+ default:
62
+ data[inp.name] = argv[inputName]
63
+ }
64
+ }
65
+ })
58
66
  }
67
+ return data
68
+ }
59
69
 
60
- // Parse arguments using minimist
61
- let argv = minimist(pargv, {
62
- alias: argvAlias,
63
- default: argvDefault,
64
- })
65
-
66
- // Set argv.inputs to the first non-option argument if it exists
67
- if (argv._.length > 0) {
68
- argv.inputs = argv._[0]
70
+ function genSchema (jsdocData) {
71
+ let schema = {
72
+ model: [],
73
+ inputs: [],
74
+ outputs: [],
69
75
  }
70
-
71
- console.log('Initial argv:', argv)
72
-
73
- let cwd = process.cwd()
74
- let inputs = argv.inputs
75
- let outputs = argv.outputs
76
- let description = argv.description
77
- let version = argv.version
78
- let ga = argv.ga
79
- let schema
80
- let descriptionTxt = ''
81
- let descriptionHtml = ''
82
- let jsdocMarkdown = ''
83
- let modelFuncs = {}
84
-
85
- // if inputs is a string with js file names, split it into an array
86
- if (typeof inputs === 'string') {
87
- if (inputs.includes('.js')) {
88
- inputs = inputs.split(',')
76
+ for (let d of jsdocData) {
77
+ const model = {
78
+ name: d.name ? d.name : d.meta.filename.split('.')[0],
79
+ description: d.description ? d.description : '',
80
+ type: d.kind,
81
+ container: 'args',
82
+ url: path.relative(process.cwd(), path.join(d.meta.path, d.meta.filename)),
83
+ worker: false
84
+ }
85
+ if (d.requires) {
86
+ model.imports = d.requires.map(r => r.replace('module:', ''))
87
+ }
88
+ if (d.params) {
89
+ // Check if all params have the same name before '.'
90
+ const names = new Set(d.params.map(p => p.name.split('.')[0]))
91
+ if ((d.params.length > 1) && (names.size === 1)) {
92
+ // Object
93
+ model.container = 'object'
94
+ d.params.slice(1).forEach(p => {
95
+ const inp = {
96
+ name: p.name.split('.')[1],
97
+ type: p.type.names[0],
98
+ description: p.description,
99
+ }
100
+ if (p.defaultvalue) {
101
+ inp.default = p.defaultvalue
102
+ }
103
+ schema.inputs.push(inp)
104
+ })
105
+ } else {
106
+ // Array
107
+ model.container = 'args'
108
+ d.params.forEach(p => {
109
+ const inp = {
110
+ name: p.name,
111
+ type: p.type.names[0],
112
+ description: p.description,
113
+ }
114
+ if (p.defaultvalue) {
115
+ inp.default = p.defaultvalue
116
+ }
117
+ schema.inputs.push(inp)
118
+ })
119
+ }
120
+ }
121
+ if (d.returns) {
122
+ d.returns.forEach(r => {
123
+ r.name = r.name ? r.name : r.description.split('-')[0].trim()
124
+ r.description = r.description.split('-').slice(1).join('-').trim()
125
+ })
126
+ const names = new Set(d.returns.map(r => r.name.split('.')[0]))
127
+ if ((d.returns.length > 1) && (names.size === 1)) {
128
+ // Object
129
+ d.returns.slice(1).forEach(p => {
130
+ const out = {
131
+ name: p.name.split('.')[1],
132
+ type: p.type.names[0],
133
+ description: p.description,
134
+ }
135
+ schema.outputs.push(out)
136
+ })
137
+ } else {
138
+ // Array
139
+ d.returns.forEach(p => {
140
+ const out = {
141
+ name: p.name,
142
+ type: p.type.names[0],
143
+ description: p.description,
144
+ }
145
+ schema.outputs.push(out)
146
+ })
147
+ }
148
+ }
149
+ if (d.customTags) {
150
+ d.customTags.forEach(t => {
151
+ if (t.tag === 'worker') {
152
+ model.worker = true
153
+ }
154
+ })
89
155
  }
156
+ schema.model.push(model)
90
157
  }
158
+ return schema
159
+ }
91
160
 
92
- // if outputs is a string with js file names, split it into an array
93
- if (typeof outputs === 'string') {
94
- outputs = outputs.split(',')
95
- }
161
+ function genHtmlFromSchema(schema) {
162
+ let htmlDescription = '<br><div class="schema-description">';
96
163
 
97
- // Check for schema.json in the current working directory
98
- if (inputs.length === 0 && fs.existsSync(path.join(cwd, 'schema.json'))) {
99
- console.log('Using schema.json from the current working directory')
100
- inputs = ['schema.json']
164
+ // Process the model section
165
+ if (schema.model && schema.model.length > 0) {
166
+ schema.model.forEach(model => {
167
+ htmlDescription += `<h3><strong>${model.name}</strong></h3>`
168
+ if (model.description) {
169
+ htmlDescription += `<p>${model.description}</p>`
170
+ }
171
+ })
101
172
  }
102
173
 
103
- if (inputs.length === 0) {
104
- console.error('No inputs provided')
105
- process.exit(1)
106
- } else if ((inputs.length === 1) && (inputs[0].includes('.json'))) {
107
- // Input is json schema
108
- // Curren working directory if not provided
109
- // schema = require(path.join(cwd, inputs[0]))
110
- // switch to fs.readFileSync to reload the schema if it changes
111
- schema = JSON.parse(fs.readFileSync(path.join(cwd, inputs[0]), 'utf8'))
112
- } else {
113
- // Array of js files
114
- // Generate schema
115
- let jsdocData = jsdoc2md.getTemplateDataSync({ files: inputs.map(f => path.join(cwd, f)) })
116
- schema = genSchema(jsdocData)
117
- // jsdocMarkdown = jsdoc2md.renderSync({
118
- // data: jsdocData,
119
- // 'param-list-format': 'list',
120
- // })
121
- }
122
-
123
- // Iterate over schema inputs and update aliases
124
- if (schema.inputs) {
125
- schema.inputs.forEach(inp => {
126
- if (inp.name) {
127
- if (inp.alias) {
128
- argvAlias[inp.name] = inp.alias
129
- }
130
- if (inp.default) {
131
- argvDefault[inp.name] = inp.default
132
- }
174
+ // Process the inputs section
175
+ if (schema.inputs && schema.inputs.length > 0) {
176
+ htmlDescription += '<h4>Inputs</h4><ul>';
177
+ schema.inputs.forEach(input => {
178
+ htmlDescription += `<li><strong>${input.name}</strong> (${input.type})`
179
+ if (input.description) {
180
+ htmlDescription += ` - ${input.description}`
133
181
  }
134
182
  })
183
+ htmlDescription += '</ul>';
135
184
  }
136
185
 
137
- // Update argv with the new aliases and defaults
138
- argv = minimist(pargv, {
139
- alias: argvAlias,
140
- default: argvDefault,
141
- })
142
-
143
- console.log('Updated argv', argv)
144
-
145
- // Initially in argv.fetch branch
146
- // Check if schema has model, convert to array if needed
147
- if (!schema.model) {
148
- console.error('No model found in schema')
149
- process.exit(1)
150
- }
151
- if (!Array.isArray(schema.model)) {
152
- schema.model = [schema.model]
153
- }
154
- if (argv.execute) {
155
- schema.model.forEach(m => {
156
- console.log('Executing model on the server:', m.name)
157
- modelFuncs[m.name] = require(path.join(cwd, m.url))
158
- m.type = 'post'
159
- m.url = `/${m.name}`
160
- m.worker = false
186
+ // Process the outputs section
187
+ if (schema.outputs && schema.outputs.length > 0) {
188
+ htmlDescription += '<h4>Outputs</h4><ul>';
189
+ schema.outputs.forEach(output => {
190
+ htmlDescription += `<li><strong>${output.name}</strong> (${output.type})`
191
+ if (output.description) {
192
+ htmlDescription += ` - ${output.description}`
193
+ }
161
194
  })
195
+ htmlDescription += '</ul>';
162
196
  }
163
- console.log('Schema:', schema)
164
-
197
+ htmlDescription += '</div>';
198
+ return htmlDescription;
199
+ }
165
200
 
166
- // Generate description block
167
- if (description) {
168
- const descriptionMd = fs.readFileSync(path.join(cwd, description), 'utf8')
169
- descriptionHtml = converter.makeHtml(descriptionMd)
201
+ function genMarkdownFromSchema(schema) {
202
+ let markdownDescription = '';
170
203
 
171
- if (descriptionMd.includes('---')) {
172
- descriptionTxt = descriptionMd
173
- .split('---')[0]
174
- .replace(/\n/g, ' ')
175
- .replace(/\s+/g, ' ')
176
- .replace(/#/g, '')
177
- .replace(/\*/g, '')
178
- .trim()
179
- }
204
+ // Process the model section
205
+ if (schema.model && schema.model.length > 0) {
206
+ schema.model.forEach(model => {
207
+ markdownDescription += `### **${model.name}**\n`;
208
+ if (model.description) {
209
+ markdownDescription += `${model.description}\n\n`;
210
+ }
211
+ });
180
212
  }
181
213
 
182
- descriptionHtml += genHtmlFromSchema(schema)
183
-
184
-
214
+ // Process the inputs section
215
+ if (schema.inputs && schema.inputs.length > 0) {
216
+ markdownDescription += '#### Inputs\n';
217
+ schema.inputs.forEach(input => {
218
+ markdownDescription += `- **${input.name}** (${input.type})`;
219
+ if (input.description) {
220
+ markdownDescription += ` - ${input.description}`;
221
+ }
222
+ markdownDescription += '\n';
223
+ });
224
+ }
185
225
 
186
- // Generate jsee code
187
- let jseeHtml = ''
188
- let hiddenElementHtml = ''
189
- if (argv.fetch) {
190
- // Fetch jsee code from the CDN or local server
191
- let jseeCode
192
- if (argv.version === 'dev') {
193
- jseeCode = fs.readFileSync(path.join(__dirname, '..', 'dist', 'jsee.js'), 'utf8')
194
- } else if (argv.version === 'latest') {
195
- jseeCode = fs.readFileSync(path.join(__dirname, '..', 'dist', 'jsee.runtime.js'), 'utf8')
196
- } else {
197
- // Pre-fetch the jsee runtime from the CDN https://cdn.jsdelivr.net/npm/@jseeio/jsee@${argv.version}/dist/jsee.runtime.js
198
- jseeCode = await fetch(`https://cdn.jsdelivr.net/npm/@jseeio/jsee@${argv.version}/dist/jsee.runtime.js`)
199
- jseeCode = await jseeCode.text()
200
- }
201
- jseeHtml = `<script>${jseeCode}</script>`
202
- // Fetch model files and store them in hidden elements
203
- hiddenElementHtml += '<div id="hidden-storage" style="display: none;">'
204
-
205
- for (let m of schema.model) {
206
- if (m.type === 'get' || m.type === 'post') {
207
- continue
208
- }
209
- if (m.url) {
210
- const modelCode = fs.readFileSync(path.join(cwd, m.url), 'utf8')
211
- hiddenElementHtml += `<script type="text/plain" style="display: none;" data-src="${m.url}">${modelCode}</script>`
226
+ // Process the outputs section
227
+ if (schema.outputs && schema.outputs.length > 0) {
228
+ markdownDescription += '#### Outputs\n';
229
+ schema.outputs.forEach(output => {
230
+ markdownDescription += `- **${output.name}** (${output.type})`;
231
+ if (output.description) {
232
+ markdownDescription += ` - ${output.description}`;
212
233
  }
213
- if (m.imports) {
214
- for (let i of m.imports) {
215
- const importUrl = i.includes('.js') ? i : `https://cdn.jsdelivr.net/npm/${i}`
216
-
217
- // Create cache directory if it doesn't exist
218
- const cacheDir = path.join(os.homedir(), '.cache', 'jsee')
219
- fs.mkdirSync(cacheDir, { recursive: true })
220
-
221
- // Create a hash of the importUrl
222
- const hash = crypto.createHash('sha256').update(importUrl).digest('hex')
223
- const cacheFilePath = path.join(cacheDir, `${hash}.js`)
224
-
225
- let importCode
226
- let useCache = false
227
-
228
- // Check if cache file exists and is less than 1 day old
229
- if (fs.existsSync(cacheFilePath)) {
230
- const stats = fs.statSync(cacheFilePath)
231
- const mtime = new Date(stats.mtime)
232
- const now = new Date()
233
- const ageInDays = (now - mtime) / (1000 * 60 * 60 * 24)
234
+ markdownDescription += '\n';
235
+ });
236
+ }
234
237
 
235
- if (ageInDays < 1) {
236
- console.log('Using cached import:', importUrl)
237
- importCode = fs.readFileSync(cacheFilePath, 'utf8');
238
- useCache = true;
239
- }
240
- }
238
+ return markdownDescription;
239
+ }
241
240
 
242
- if (!useCache) {
243
- const response = await fetch(importUrl);
244
- if (!response.ok) {
245
- console.error(`Failed to fetch ${importUrl}: ${response.statusText}`);
246
- process.exit(1);
247
- }
248
- importCode = await response.text()
249
- fs.writeFileSync(cacheFilePath, importCode, 'utf8')
250
- console.log('Fetched and stored to cache:', importUrl)
251
- }
252
- hiddenElementHtml += `<script type="text/plain" style="display: none;" data-src="${importUrl}">${importCode}</script>`
253
- }
254
- }
241
+ function template(schema, blocks) {
242
+ let title = 'jsee'
243
+ // let url = schema.page && schema.page.url ? schema.page.url : ''
244
+ let url = ('page' in schema && 'url' in schema.page) ? schema.page.url : ''
245
+ if (schema.title) {
246
+ title = schema.title
247
+ } else if (schema.page && schema.page.title) {
248
+ title = schema.page.title
249
+ } else if (schema.model) {
250
+ if (Array.isArray(schema.model)) {
251
+ title = schema.model[0].name
252
+ } else {
253
+ title = schema.model.name
255
254
  }
256
- hiddenElementHtml += '</div>'
257
- } else {
258
- jseeHtml = outputs
259
- ? `<script src="https://cdn.jsdelivr.net/npm/@jseeio/jsee@${argv.version}/dist/jsee.runtime.js"></script>`
260
- : `<script src="http://localhost:${argv.port}/dist/jsee.runtime.js"></script>`
261
255
  }
262
256
 
263
- let socialHtml = ''
264
- let gaHtml = ''
265
- let orgHtml = ''
257
+ return `<!DOCTYPE html>
266
258
 
267
- if (schema.page) {
268
- if (schema.page.title) {
269
- title = schema.page.title
270
- }
271
- if (schema.page.ga) {
272
- gaHtml = `
273
- <script id="ga-src" async src="https://www.googletagmanager.com/gtag/js?id=${schema.page.ga}"></script>
274
- <script id="ga-body">
275
- window['ga-disable-${schema.page.ga}'] = window.doNotTrack === "1" || navigator.doNotTrack === "1" || navigator.doNotTrack === "yes" || navigator.msDoNotTrack === "1";
276
- window.dataLayer = window.dataLayer || [];
277
- function gtag(){dataLayer.push(arguments);}
278
- gtag('js', new Date());
279
- gtag('config', '${schema.page.ga}');
280
- </script>
281
- `
282
- }
259
+ <!-- Generated by JSEE (https://jsee.org) -->
260
+ <!-- Do not edit this file directly. Edit the source files and run jsee to generate this file. -->
261
+ <!-- License: MIT (https://opensource.org/licenses/MIT) -->
283
262
 
284
- // Social media links
285
- if (schema.page.social) {
286
- // iterate over dict with k, v pairs
287
- for (let [name, url] of Object.entries(schema.page.social)) {
288
- switch (name) {
289
- case 'twitter':
290
- socialHtml += `<li><a rel="me" href="https://twitter.com/${url}">Twitter</a></li>`
291
- break
292
- case 'github':
293
- socialHtml += `<li><a rel="me" href="https://github.com/${url}">GitHub</a></li>`
294
- break
295
- case 'facebook':
296
- socialHtml += `<li><a rel="me" href="https://www.facebook.com/${url}">Facebook</a></li>`
297
- break
298
- case 'linkedin':
299
- socialHtml += `<li><a rel="me" href="https://www.linkedin.com/company/${url}">LinkedIn</a></li>`
300
- break
301
- case 'instagram':
302
- socialHtml += `<li><a rel="me" href="https://www.instagram.com/${url}">Instagram</a></li>`
303
- break
304
- case 'youtube':
305
- socialHtml += `<li><a rel="me" href="https://www.youtube.com/${url}">YouTube</a></li>`
306
- break
307
- default:
308
- socialHtml += `<li><a rel="me" href="${s.url}">${s.name}</a></li>`
309
- }
310
- }
311
- }
263
+ <html lang="en">
264
+ <head>
265
+ <meta charset="utf-8">
266
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
267
+ <meta name="viewport" content="width=device-width, initial-scale=1">
268
+ <title>${title}</title>
269
+ <meta name="description" content="${blocks.descriptionTxt}">
312
270
 
313
- if (schema.page.org) {
314
- orgHtml = `<div class="footer-org"><h4 class="footer-heading"><a href="${schema.page.org.url}">${schema.page.org.name}</a></h4>`
315
- if (schema.page.org.description) {
316
- orgHtml += `<p>${schema.page.org.description}</p>`
317
- }
318
- orgHtml += '</div>'
319
- }
271
+ <!-- Open Graph -->
272
+ <meta property="og:title" content="${title}" />
273
+ <meta property="og:description" content="${blocks.descriptionTxt}" />
274
+ <meta property="og:locale" content="en_US" />
275
+ <meta property="og:url" content="${url}" />
276
+ <meta property="og:site_name" content="${title}" />
277
+ <meta property="og:type" content="website" />
320
278
 
321
- }
279
+ <!-- Twitter Card -->
280
+ <meta name="twitter:card" content="summary" />
281
+ <meta name="twitter:title" content="${title}" />
282
+ <meta name="twitter:description" content="${blocks.descriptionTxt}" />
322
283
 
323
- const html = template(schema, {
324
- descriptionHtml: pad(descriptionHtml, 8, 1),
325
- descriptionTxt: descriptionTxt,
326
- gaHtml: pad(gaHtml, 2, 1),
327
- jseeHtml: jseeHtml,
328
- hiddenElementHtml: hiddenElementHtml,
329
- socialHtml: pad(socialHtml, 2, 1),
330
- orgHtml: pad(orgHtml, 2, 1),
331
- })
284
+ <!-- Structured Data -->
285
+ <script type="application/ld+json">{"@context":"https://schema.org","@type":"WebSite","headline":"${title}","name":"${title}","url":"${url}", "description":"${blocks.descriptionTxt}"}</script>
332
286
 
333
- if (returnHtml) {
334
- // Return the html as a string
335
- return html
336
- } else if (outputs) {
337
- // Store the html in the output file
338
- for (let o of outputs) {
339
- if (o === 'stdout') {
340
- console.log(html)
341
- } else if (o.includes('.html')) {
342
- fs.writeFileSync(path.join(cwd, o), html)
343
- } else if (o.includes('.json')) {
344
- fs.writeFileSync(path.join(cwd, o), JSON.stringify(schema, null, 2))
345
- } else if (o.includes('.md')) {
346
- fs.writeFileSync(path.join(cwd, o), genMarkdownFromSchema(schema))
347
- } else {
348
- console.error('Invalid output file:', o)
349
- }
350
- }
351
- fs.writeFileSync(path.join(cwd, outputs[0]), html)
352
- } else {
353
- // Serve the html
354
- const express = require('express')
355
- const app = express()
356
- app.use(express.json())
357
- if (argv.execute) {
358
- // Create post endpoint for executing the model
359
- schema.model.forEach(m => {
360
- app.post(m.url, (req, res) => {
361
- console.log(`Executing model: ${m.name}`)
362
- if (m.name in modelFuncs) {
363
- const modelFunc = modelFuncs[m.name]
364
- try {
365
- const result = modelFunc(req.body)
366
- res.json(result)
367
- } catch (error) {
368
- console.error('Error executing model:', error)
369
- res.status(500).json({ error: error.message })
370
- }
371
- }
372
- })
373
- console.log('Model execution endpoints created:', m.url)
374
- })
375
- }
376
- app.get('/', async (req, res) => {
377
- console.log('Serving index.html')
378
- res.send(await gen(process.argv, true))
379
- })
380
- app.get('/dist/jsee.runtime.js', (req, res) => {
381
- const pathToJSEE = path.join(__dirname, '..', 'dist', 'jsee.runtime.js')
382
- console.log(`Serving jsee.runtime.js from ${pathToJSEE}`)
383
- res.sendFile(pathToJSEE)
384
- })
385
- app.use(express.static(cwd))
386
- app.listen(argv.port, () => {
387
- console.log(`JSEE app is running: http://localhost:${argv.port}`)
388
- })
389
- }
390
- }
391
-
392
- function genSchema (jsdocData) {
393
- let schema = {
394
- model: [],
395
- inputs: [],
396
- outputs: [],
397
- }
398
- for (let d of jsdocData) {
399
- const model = {
400
- name: d.name ? d.name : d.meta.filename.split('.')[0],
401
- description: d.description ? d.description : '',
402
- type: d.kind,
403
- container: 'args',
404
- url: path.relative(process.cwd(), path.join(d.meta.path, d.meta.filename)),
405
- worker: false
406
- }
407
- if (d.requires) {
408
- model.imports = d.requires.map(r => r.replace('module:', ''))
409
- }
410
- if (d.params) {
411
- // Check if all params have the same name before '.'
412
- const names = new Set(d.params.map(p => p.name.split('.')[0]))
413
- if ((d.params.length > 1) && (names.size === 1)) {
414
- // Object
415
- model.container = 'object'
416
- d.params.slice(1).forEach(p => {
417
- const inp = {
418
- name: p.name.split('.')[1],
419
- type: p.type.names[0],
420
- description: p.description,
421
- }
422
- if (p.defaultvalue) {
423
- inp.default = p.defaultvalue
424
- }
425
- schema.inputs.push(inp)
426
- })
427
- } else {
428
- // Array
429
- model.container = 'args'
430
- d.params.forEach(p => {
431
- const inp = {
432
- name: p.name,
433
- type: p.type.names[0],
434
- description: p.description,
435
- }
436
- if (p.defaultvalue) {
437
- inp.default = p.defaultvalue
438
- }
439
- schema.inputs.push(inp)
440
- })
441
- }
442
- }
443
- if (d.returns) {
444
- d.returns.forEach(r => {
445
- r.name = r.name ? r.name : r.description.split('-')[0].trim()
446
- r.description = r.description.split('-').slice(1).join('-').trim()
447
- })
448
- const names = new Set(d.returns.map(r => r.name.split('.')[0]))
449
- if ((d.returns.length > 1) && (names.size === 1)) {
450
- // Object
451
- d.returns.slice(1).forEach(p => {
452
- const out = {
453
- name: p.name.split('.')[1],
454
- type: p.type.names[0],
455
- description: p.description,
456
- }
457
- schema.outputs.push(out)
458
- })
459
- } else {
460
- // Array
461
- d.returns.forEach(p => {
462
- const out = {
463
- name: p.name,
464
- type: p.type.names[0],
465
- description: p.description,
466
- }
467
- schema.outputs.push(out)
468
- })
469
- }
470
- }
471
- if (d.customTags) {
472
- d.customTags.forEach(t => {
473
- if (t.tag === 'worker') {
474
- model.worker = true
475
- }
476
- })
477
- }
478
- schema.model.push(model)
479
- }
480
- return schema
481
- }
482
-
483
- function genHtmlFromSchema(schema) {
484
- let htmlDescription = '<br><div class="schema-description">';
485
-
486
- // Process the model section
487
- if (schema.model && schema.model.length > 0) {
488
- schema.model.forEach(model => {
489
- htmlDescription += `<h3><strong>${model.name}</strong></h3>`
490
- if (model.description) {
491
- htmlDescription += `<p>${model.description}</p>`
492
- }
493
- })
494
- }
495
-
496
- // Process the inputs section
497
- if (schema.inputs && schema.inputs.length > 0) {
498
- htmlDescription += '<h4>Inputs</h4><ul>';
499
- schema.inputs.forEach(input => {
500
- htmlDescription += `<li><strong>${input.name}</strong> (${input.type})`
501
- if (input.description) {
502
- htmlDescription += ` - ${input.description}`
503
- }
504
- })
505
- htmlDescription += '</ul>';
506
- }
507
-
508
- // Process the outputs section
509
- if (schema.outputs && schema.outputs.length > 0) {
510
- htmlDescription += '<h4>Outputs</h4><ul>';
511
- schema.outputs.forEach(output => {
512
- htmlDescription += `<li><strong>${output.name}</strong> (${output.type})`
513
- if (output.description) {
514
- htmlDescription += ` - ${output.description}`
515
- }
516
- })
517
- htmlDescription += '</ul>';
518
- }
519
- htmlDescription += '</div>';
520
- return htmlDescription;
521
- }
522
-
523
- function genMarkdownFromSchema(schema) {
524
- let markdownDescription = '';
525
-
526
- // Process the model section
527
- if (schema.model && schema.model.length > 0) {
528
- schema.model.forEach(model => {
529
- markdownDescription += `### **${model.name}**\n`;
530
- if (model.description) {
531
- markdownDescription += `${model.description}\n\n`;
532
- }
533
- });
534
- }
535
-
536
- // Process the inputs section
537
- if (schema.inputs && schema.inputs.length > 0) {
538
- markdownDescription += '#### Inputs\n';
539
- schema.inputs.forEach(input => {
540
- markdownDescription += `- **${input.name}** (${input.type})`;
541
- if (input.description) {
542
- markdownDescription += ` - ${input.description}`;
543
- }
544
- markdownDescription += '\n';
545
- });
546
- }
547
-
548
- // Process the outputs section
549
- if (schema.outputs && schema.outputs.length > 0) {
550
- markdownDescription += '#### Outputs\n';
551
- schema.outputs.forEach(output => {
552
- markdownDescription += `- **${output.name}** (${output.type})`;
553
- if (output.description) {
554
- markdownDescription += ` - ${output.description}`;
555
- }
556
- markdownDescription += '\n';
557
- });
558
- }
559
-
560
- return markdownDescription;
561
- }
562
-
563
- function template(schema, blocks) {
564
- let title = 'jsee'
565
- // let url = schema.page && schema.page.url ? schema.page.url : ''
566
- let url = ('page' in schema && 'url' in schema.page) ? schema.page.url : ''
567
- if (schema.title) {
568
- title = schema.title
569
- } else if (schema.page && schema.page.title) {
570
- title = schema.page.title
571
- } else if (schema.model) {
572
- if (Array.isArray(schema.model)) {
573
- title = schema.model[0].name
574
- } else {
575
- title = schema.model.name
576
- }
577
- }
578
-
579
- return `<!DOCTYPE html>
580
-
581
- <!-- Generated by JSEE (https://jsee.org) -->
582
- <!-- Do not edit this file directly. Edit the source files and run jsee to generate this file. -->
583
- <!-- License: MIT (https://opensource.org/licenses/MIT) -->
584
-
585
- <html lang="en">
586
- <head>
587
- <meta charset="utf-8">
588
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
589
- <meta name="viewport" content="width=device-width, initial-scale=1">
590
- <title>${title}</title>
591
- <meta name="description" content="${blocks.descriptionTxt}">
592
-
593
- <!-- Open Graph -->
594
- <meta property="og:title" content="${title}" />
595
- <meta property="og:description" content="${blocks.descriptionTxt}" />
596
- <meta property="og:locale" content="en_US" />
597
- <meta property="og:url" content="${url}" />
598
- <meta property="og:site_name" content="${title}" />
599
- <meta property="og:type" content="website" />
600
-
601
- <!-- Twitter Card -->
602
- <meta name="twitter:card" content="summary" />
603
- <meta name="twitter:title" content="${title}" />
604
- <meta name="twitter:description" content="${blocks.descriptionTxt}" />
605
-
606
- <!-- Structured Data -->
607
- <script type="application/ld+json">{"@context":"https://schema.org","@type":"WebSite","headline":"${title}","name":"${title}","url":"${url}", "description":"${blocks.descriptionTxt}"}</script>
608
-
609
- <!-- Canonical Link -->
610
- <link rel="canonical" href="${url}" />
287
+ <!-- Canonical Link -->
288
+ <link rel="canonical" href="${url}" />
611
289
 
612
290
  <!-- Favicon -->
613
291
  <link href="data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAgAAAAAAAAAAAAAAAEAAAAAAAAAD9/f0AAAAAAPj4+AAMDAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEQAAAAAAABERAAAAAAAAEREAAAAAAAAREQABERESABERAAETMzAAEREAARAAAAAREQABEAAAABERAAEQARAAEREAARABEAAREQABEAAAABERAAEQAAAAEREAAREREAAREQABEREQABERAAAAAAAAEREAAAAAAAAREQAAAAAAABHAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAA" rel="icon" type="image/x-icon" />
@@ -640,7 +318,7 @@ function template(schema, blocks) {
640
318
  pre { padding: 8px 12px; overflow-x: auto; }
641
319
  pre > code { border: 0; padding-right: 0; padding-left: 0; }
642
320
  .wrapper { max-width: calc(800px - (30px)); margin-right: auto; margin-left: auto; padding-right: 15px; padding-left: 15px; }
643
- @media screen and (min-width: 800px) { .wrapper { max-width: calc(800px - (30px * 2)); padding-right: 30px; padding-left: 30px; } }
321
+ @media screen and (min-width: 800px) { .wrapper { max-width: calc(1024px - (30px * 2)); padding-right: 30px; padding-left: 30px; } }
644
322
  .wrapper:after { content: ""; display: table; clear: both; }
645
323
  .orange { color: #f66a0a; }
646
324
  .grey { color: #828282; }
@@ -783,4 +461,440 @@ function template(schema, blocks) {
783
461
  </html>`
784
462
  }
785
463
 
464
+ // Adding async here breaks express. TODO: investigate
465
+ async function gen (pargv, returnHtml=false) {
466
+ // Determine if JSEE CLI is imported or run directly
467
+ const imported = path.dirname(__dirname) !== path.dirname(require.main.path)
468
+
469
+ // First pass over CLI arguments
470
+ // JSEE-level args
471
+ const argvAlias = {
472
+ inputs: 'i',
473
+ outputs: 'o',
474
+ description: 'd',
475
+ port: 'p',
476
+ version: 'v',
477
+ fetch: 'f',
478
+ execute: 'e',
479
+ cdn: 'c',
480
+ }
481
+ const argvDefault = {
482
+ execute: false, // execute the model code on the server
483
+ fetch: false, // fetch the JSEE runtime from the CDN or local server
484
+ inputs: 'schema.json', // default input is schema.json in the current working directory
485
+ port: 3000, // default port for the server
486
+ version: 'latest', // default version of JSEE runtime to use
487
+ verbose: false, // verbose mode
488
+ cdn: false,
489
+ }
490
+ let argv = minimist(pargv, {
491
+ alias: argvAlias,
492
+ default: argvDefault,
493
+ })
494
+ // Set argv.inputs to the first non-option argument if it exists
495
+ if (!imported && argv._.length > 0 && !argv.inputs) {
496
+ argv.inputs = argv._[0]
497
+ }
498
+
499
+ function log (...args) {
500
+ if (argv.verbose) {
501
+ console.log('[JSEE CLI]', ...args)
502
+ }
503
+ }
504
+
505
+ log('Imported:', imported)
506
+ log('Current working directory:', process.cwd())
507
+ log('Script location:', __dirname)
508
+ log('Script file:', __filename)
509
+ log('Require location:', require.main.path)
510
+ log('Require file:', require.main.filename)
511
+
512
+ let cwd = process.cwd()
513
+ let inputs = argv.inputs
514
+ let outputs = argv.outputs
515
+ let description = argv.description
516
+ let schema
517
+ let schemaPath
518
+ let descriptionTxt = ''
519
+ let descriptionHtml = ''
520
+ let jsdocMarkdown = ''
521
+ let modelFuncs = {}
522
+
523
+ // Determine the inputs and outputs
524
+ // if inputs is a string with js file names, split it into an array
525
+ if (typeof inputs === 'string') {
526
+ if (inputs.includes('.js')) {
527
+ inputs = inputs.split(',')
528
+ }
529
+ }
530
+
531
+ // if outputs is a string with js file names, split it into an array
532
+ if (typeof outputs === 'string') {
533
+ outputs = outputs.split(',')
534
+ }
535
+
536
+ if (inputs.length === 0) {
537
+ console.error('No inputs provided')
538
+ process.exit(1)
539
+ } else if ((inputs.length === 1) && (inputs[0].includes('.json'))) {
540
+ // Input is json schema
541
+ // Curren working directory if not provided
542
+ // schema = require(path.join(cwd, inputs[0]))
543
+ // switch to fs.readFileSync to reload the schema if it changes
544
+ schemaPath = inputs[0].startsWith('/') ? inputs[0] : path.join(cwd, inputs[0])
545
+ schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'))
546
+ } else {
547
+ // Array of js files
548
+ // Generate schema
549
+ let jsdocData = jsdoc2md.getTemplateDataSync({ files: inputs.map(f => path.join(cwd, f)) })
550
+ schema = genSchema(jsdocData)
551
+ // jsdocMarkdown = jsdoc2md.renderSync({
552
+ // data: jsdocData,
553
+ // 'param-list-format': 'list',
554
+ // })
555
+ }
556
+ log('Schema path:', schemaPath)
557
+
558
+ // Second pass over CLI arguments
559
+ // Iterate over schema inputs and update argv aliases
560
+ if (schema.inputs) {
561
+ schema.inputs.forEach((inp, inp_index) => {
562
+ if (inp.name) {
563
+ const inputName = sanitizeName(inp.name)
564
+ if (inp.alias) {
565
+ argvAlias[inputName] = inp.alias
566
+ }
567
+ // Use positional arguments as schema inputs defaults if JSEE CLI is imported
568
+ if (imported && argv._.length > inp_index) {
569
+ log('Using positional argument for input:', inputName, argv._[inp_index])
570
+ argvDefault[inputName] = argv._[inp_index]
571
+ }
572
+ // We don't need to duplicate defaults here, as we handle them on the frontend
573
+ // else if (inp.default) {
574
+ // argvDefault[inputName] = inp.default
575
+ // }
576
+ }
577
+ })
578
+ }
579
+ // Update argv with the new aliases and defaults
580
+ argv = minimist(pargv, {
581
+ alias: argvAlias,
582
+ default: argvDefault,
583
+ })
584
+
585
+ // Now deactivate the inputs present in argv
586
+ // If you set parameter on the command line, it should not be editable in the GUI
587
+ // E.g. file selected
588
+ const dataFromArgvWithoutFileLoading = getDataFromArgv(schema, argv, false)
589
+ log('Data from argv without file loading:', dataFromArgvWithoutFileLoading)
590
+ if (schema.inputs) {
591
+ schema.inputs.forEach(inp => {
592
+ // Here data contains unsanitized input names
593
+ if (inp.name in dataFromArgvWithoutFileLoading) {
594
+ inp.default = dataFromArgvWithoutFileLoading[inp.name]
595
+ inp.disabled = true // Deactivate the input if it's present in argv
596
+ }
597
+ })
598
+ }
599
+ log('Argv:', argv)
600
+
601
+ // Initially in argv.fetch branch
602
+ // Check if schema has model, convert to array if needed
603
+ if (!schema.model) {
604
+ // console.error('No model found in schema')
605
+ // process.exit(1)
606
+ // It's still valid schema, can be only render function or vis of inputs/outputs
607
+ schema.model = []
608
+ }
609
+ if (!Array.isArray(schema.model)) {
610
+ schema.model = [schema.model]
611
+ }
612
+
613
+ // Server-side execution
614
+ // If execute is true, we will prepare the model functions to run on the server side
615
+ // Schema model will be updated with the server url and POST method
616
+ if (argv.execute) {
617
+ await Promise.all(schema.model.map(async m => {
618
+ log('Preparing a model to run on the server side:', m.name, m.url)
619
+ const target = require(path.join(schemaPath ? path.dirname(schemaPath) : cwd, m.url))
620
+ modelFuncs[m.name] = await getModelFuncJS(m, target, {log})
621
+ m.type = 'post'
622
+ m.url = `/${m.name}`
623
+ m.worker = false
624
+ }))
625
+ }
626
+
627
+ // Switch to CDN for model files
628
+ if (argv.cdn) {
629
+ let cdn = ''
630
+ console.log(argv)
631
+ if (typeof argv.cdn === 'string') {
632
+ cdn = argv.cdn
633
+ } else if (typeof argv.cdn === 'boolean') {
634
+ // Check package.json in cwd
635
+ const packageJsonPath = path.join(cwd, 'package.json')
636
+ if (fs.existsSync(packageJsonPath)) {
637
+ const target = require(packageJsonPath)
638
+ const packageName = target.name
639
+ cdn = `https://cdn.jsdelivr.net/npm/${packageName}@${target.version}/`
640
+ } else {
641
+ console.error(`No package.json found: ${packageJsonPath}`)
642
+ process.exit(1)
643
+ }
644
+ } else {
645
+ console.error('Invalid CDN argument. Use --cdn <url> or --cdn true to use package.json version.')
646
+ process.exit(1)
647
+ }
648
+ log('Using CDN for model files:', cdn)
649
+ schema.model.forEach(m => {
650
+ if (m.url) {
651
+ // If url is relative, make it absolute
652
+ if (!m.url.startsWith('http')) {
653
+ m.url = path.join(cdn, m.url)
654
+ log(`Updated ${m.name} model URL to: ${m.url}`)
655
+ }
656
+ }
657
+ })
658
+ }
659
+
660
+ log('Schema:', schema)
661
+
662
+ // Generate description block
663
+ if (description) {
664
+ const descriptionMd = fs.readFileSync(path.join(cwd, description), 'utf8')
665
+ descriptionHtml = converter.makeHtml(descriptionMd)
666
+
667
+ if (descriptionMd.includes('---')) {
668
+ descriptionTxt = descriptionMd
669
+ .split('---')[0]
670
+ .replace(/\n/g, ' ')
671
+ .replace(/\s+/g, ' ')
672
+ .replace(/#/g, '')
673
+ .replace(/\*/g, '')
674
+ .trim()
675
+ }
676
+ }
677
+ descriptionHtml += genHtmlFromSchema(schema)
678
+
679
+ // Generate jsee code
680
+ let jseeHtml = ''
681
+ let hiddenElementHtml = ''
682
+ if (argv.fetch) {
683
+ // Fetch jsee code from the CDN or local server
684
+ let jseeCode
685
+ if (argv.version === 'dev') {
686
+ jseeCode = fs.readFileSync(path.join(__dirname, '..', 'dist', 'jsee.js'), 'utf8')
687
+ } else if (argv.version === 'latest') {
688
+ jseeCode = fs.readFileSync(path.join(__dirname, '..', 'dist', 'jsee.runtime.js'), 'utf8')
689
+ } else {
690
+ // Pre-fetch the jsee runtime from the CDN https://cdn.jsdelivr.net/npm/@jseeio/jsee@${argv.version}/dist/jsee.runtime.js
691
+ jseeCode = await fetch(`https://cdn.jsdelivr.net/npm/@jseeio/jsee@${argv.version}/dist/jsee.runtime.js`)
692
+ jseeCode = await jseeCode.text()
693
+ }
694
+ jseeHtml = `<script>${jseeCode}</script>`
695
+ // Fetch model files and store them in hidden elements
696
+ hiddenElementHtml += '<div id="hidden-storage" style="display: none;">'
697
+
698
+ for (let m of schema.model) {
699
+ if (m.type === 'get' || m.type === 'post') {
700
+ continue
701
+ }
702
+ if (m.url) {
703
+ // Fetch model from the local file system (url)
704
+ // TODO: Can be a remote URL (e.g. CDN)
705
+ const modelCode = fs.readFileSync(path.join(cwd, m.url), 'utf8')
706
+ hiddenElementHtml += `<script type="text/plain" style="display: none;" data-src="${m.url}">${modelCode}</script>`
707
+ }
708
+ if (m.imports) {
709
+ for (let i of m.imports) {
710
+ const importUrl = i.includes('.js') ? i : `https://cdn.jsdelivr.net/npm/${i}`
711
+
712
+ // Create cache directory if it doesn't exist
713
+ const cacheDir = path.join(os.homedir(), '.cache', 'jsee')
714
+ fs.mkdirSync(cacheDir, { recursive: true })
715
+
716
+ // Create a hash of the importUrl
717
+ const hash = crypto.createHash('sha256').update(importUrl).digest('hex')
718
+ const cacheFilePath = path.join(cacheDir, `${hash}.js`)
719
+
720
+ let importCode
721
+ let useCache = false
722
+
723
+ // Check if cache file exists and is less than 1 day old
724
+ if (fs.existsSync(cacheFilePath)) {
725
+ const stats = fs.statSync(cacheFilePath)
726
+ const mtime = new Date(stats.mtime)
727
+ const now = new Date()
728
+ const ageInDays = (now - mtime) / (1000 * 60 * 60 * 24)
729
+
730
+ if (ageInDays < 1) {
731
+ log('Using cached import:', importUrl)
732
+ importCode = fs.readFileSync(cacheFilePath, 'utf8');
733
+ useCache = true;
734
+ }
735
+ }
736
+
737
+ if (!useCache) {
738
+ const response = await fetch(importUrl);
739
+ if (!response.ok) {
740
+ console.error(`Failed to fetch ${importUrl}: ${response.statusText}`);
741
+ process.exit(1);
742
+ }
743
+ importCode = await response.text()
744
+ fs.writeFileSync(cacheFilePath, importCode, 'utf8')
745
+ log('Fetched and stored to cache:', importUrl)
746
+ }
747
+ hiddenElementHtml += `<script type="text/plain" style="display: none;" data-src="${importUrl}">${importCode}</script>`
748
+ }
749
+ }
750
+ }
751
+ hiddenElementHtml += '</div>'
752
+ } else {
753
+ jseeHtml = outputs
754
+ ? `<script src="https://cdn.jsdelivr.net/npm/@jseeio/jsee@${argv.version}/dist/jsee.runtime.js"></script>`
755
+ : argv.version === 'dev'
756
+ ? `<script src="http://localhost:${argv.port}/dist/jsee.js"></script>`
757
+ : `<script src="http://localhost:${argv.port}/dist/jsee.runtime.js"></script>`
758
+ }
759
+
760
+ let socialHtml = ''
761
+ let gaHtml = ''
762
+ let orgHtml = ''
763
+
764
+ if (schema.page) {
765
+ if (schema.page.title) {
766
+ title = schema.page.title
767
+ }
768
+ if (schema.page.ga) {
769
+ gaHtml = `
770
+ <script id="ga-src" async src="https://www.googletagmanager.com/gtag/js?id=${schema.page.ga}"></script>
771
+ <script id="ga-body">
772
+ window['ga-disable-${schema.page.ga}'] = window.doNotTrack === "1" || navigator.doNotTrack === "1" || navigator.doNotTrack === "yes" || navigator.msDoNotTrack === "1";
773
+ window.dataLayer = window.dataLayer || [];
774
+ function gtag(){dataLayer.push(arguments);}
775
+ gtag('js', new Date());
776
+ gtag('config', '${schema.page.ga}');
777
+ </script>
778
+ `
779
+ }
780
+
781
+ // Social media links
782
+ if (schema.page.social) {
783
+ // iterate over dict with k, v pairs
784
+ for (let [name, url] of Object.entries(schema.page.social)) {
785
+ switch (name) {
786
+ case 'twitter':
787
+ socialHtml += `<li><a rel="me" href="https://twitter.com/${url}">Twitter</a></li>`
788
+ break
789
+ case 'github':
790
+ socialHtml += `<li><a rel="me" href="https://github.com/${url}">GitHub</a></li>`
791
+ break
792
+ case 'facebook':
793
+ socialHtml += `<li><a rel="me" href="https://www.facebook.com/${url}">Facebook</a></li>`
794
+ break
795
+ case 'linkedin':
796
+ socialHtml += `<li><a rel="me" href="https://www.linkedin.com/company/${url}">LinkedIn</a></li>`
797
+ break
798
+ case 'instagram':
799
+ socialHtml += `<li><a rel="me" href="https://www.instagram.com/${url}">Instagram</a></li>`
800
+ break
801
+ case 'youtube':
802
+ socialHtml += `<li><a rel="me" href="https://www.youtube.com/${url}">YouTube</a></li>`
803
+ break
804
+ default:
805
+ socialHtml += `<li><a rel="me" href="${s.url}">${s.name}</a></li>`
806
+ }
807
+ }
808
+ }
809
+
810
+ if (schema.page.org) {
811
+ orgHtml = `<div class="footer-org"><h4 class="footer-heading"><a href="${schema.page.org.url}">${schema.page.org.name}</a></h4>`
812
+ if (schema.page.org.description) {
813
+ orgHtml += `<p>${schema.page.org.description}</p>`
814
+ }
815
+ orgHtml += '</div>'
816
+ }
817
+
818
+ }
819
+
820
+ const html = template(schema, {
821
+ descriptionHtml: pad(descriptionHtml, 8, 1),
822
+ descriptionTxt: descriptionTxt,
823
+ gaHtml: pad(gaHtml, 2, 1),
824
+ jseeHtml: jseeHtml,
825
+ hiddenElementHtml: hiddenElementHtml,
826
+ socialHtml: pad(socialHtml, 2, 1),
827
+ orgHtml: pad(orgHtml, 2, 1),
828
+ })
829
+
830
+ if (returnHtml) {
831
+ // Return the html as a string
832
+ return html
833
+ } else if (outputs) {
834
+ // Store the html in the output file
835
+ for (let o of outputs) {
836
+ if (o === 'stdout') {
837
+ log(html)
838
+ } else if (o.includes('.html')) {
839
+ fs.writeFileSync(path.join(cwd, o), html)
840
+ } else if (o.includes('.json')) {
841
+ fs.writeFileSync(path.join(cwd, o), JSON.stringify(schema, null, 2))
842
+ } else if (o.includes('.md')) {
843
+ fs.writeFileSync(path.join(cwd, o), genMarkdownFromSchema(schema))
844
+ } else {
845
+ console.error('Invalid output file:', o)
846
+ }
847
+ }
848
+ fs.writeFileSync(path.join(cwd, outputs[0]), html)
849
+ } else {
850
+ // Serve the html
851
+ const express = require('express')
852
+ const app = express()
853
+ app.use(express.json())
854
+ if (argv.execute) {
855
+ // Create post endpoint for executing the model
856
+ schema.model.forEach(m => {
857
+ app.post(m.url, (req, res) => {
858
+ log(`Executing model: ${m.name}`)
859
+ if (m.name in modelFuncs) {
860
+ const modelFunc = modelFuncs[m.name]
861
+ try {
862
+ const dataFromArgv = getDataFromArgv(schema, argv)
863
+ const dataFromGUI = req.body
864
+ const data = { ...dataFromGUI, ...dataFromArgv }
865
+ log('Data for model execution:', data)
866
+ const result = modelFunc(data)
867
+ res.json(result)
868
+ log(`Model ${m.name} executed successfully: `, result)
869
+ } catch (error) {
870
+ console.error('Error executing model:', error)
871
+ res.status(500).json({ error: error.message })
872
+ }
873
+ }
874
+ })
875
+ log('Model execution endpoints created:', m.url)
876
+ })
877
+ }
878
+ app.get('/', async (req, res) => {
879
+ log('Serving index.html')
880
+ res.send(await gen(pargv, true))
881
+ })
882
+ // app.get('/dist/jsee.runtime.js', (req, res) => {
883
+ // // __dirname points to this file location (it's jsee/src/cli.js, likely in node_modules)
884
+ // // so we need to go up one level to get to the dist folder with jsee.runtime.js
885
+ // const pathToJSEE = path.join(__dirname, '..', 'dist', 'jsee.runtime.js')
886
+ // log(`Serving jsee.runtime.js from: ${pathToJSEE}`)
887
+ // res.sendFile(pathToJSEE)
888
+ // })
889
+ app.use('/dist', express.static(path.join(__dirname, '..', 'dist'))) // Serve static files from the dist folder
890
+ // app.use(express.static(cwd))
891
+ // app.use(express.static(require.main.path)) // Serve static files from the main module path
892
+ // app.use(express.static(path.join(require.main.path, '..'))) // Serve static files from the parent directory of the main module path
893
+ app.use(express.static(schemaPath ? path.dirname(schemaPath) : cwd)) // Serve static files from the schema path or current working directory
894
+ app.listen(argv.port, () => {
895
+ console.log(`JSEE app is running: http://localhost:${argv.port}`)
896
+ })
897
+ }
898
+ }
899
+
786
900
  module.exports = gen