@jseeio/jsee 0.3.7 → 0.3.9

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,364 +31,146 @@ 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)
34
+ function toArray (value) {
35
+ if (!value) return []
36
+ return Array.isArray(value) ? value : [value]
37
+ }
41
38
 
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'
58
- }
39
+ function collectFetchBundleBlocks (schema) {
40
+ return []
41
+ .concat(toArray(schema.model))
42
+ .concat(toArray(schema.view))
43
+ .concat(toArray(schema.render))
44
+ .filter(Boolean)
45
+ }
59
46
 
60
- // Parse arguments using minimist
61
- let argv = minimist(pargv, {
62
- alias: argvAlias,
63
- default: argvDefault,
64
- })
47
+ function isHttpUrl (value) {
48
+ return /^https?:\/\//i.test(value)
49
+ }
65
50
 
66
- // Set argv.inputs to the first non-option argument if it exists
67
- if (argv._.length > 0) {
68
- argv.inputs = argv._[0]
51
+ function toRuntimeUrl (value) {
52
+ try {
53
+ return (new URL(value)).href
54
+ } catch (error) {
55
+ return (new URL(value, 'https://cdn.jsdelivr.net/npm/')).href
69
56
  }
57
+ }
70
58
 
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 = {}
59
+ function isLocalJsImport (value) {
60
+ if (typeof value !== 'string') return false
61
+ const lower = value.toLowerCase()
62
+ if (!lower.endsWith('.js') && !lower.includes('.js?')) return false
63
+ if (isHttpUrl(value)) return false
64
+ return value.startsWith('./') || value.startsWith('../') || value.startsWith('/') || value.startsWith('file://')
65
+ }
84
66
 
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(',')
89
- }
67
+ function getImportUrlValue (importValue) {
68
+ if (typeof importValue === 'string') return importValue
69
+ if (importValue && typeof importValue === 'object' && typeof importValue.url === 'string') {
70
+ return importValue.url
90
71
  }
72
+ return null
73
+ }
91
74
 
92
- // if outputs is a string with js file names, split it into an array
93
- if (typeof outputs === 'string') {
94
- outputs = outputs.split(',')
75
+ function resolveFetchImport (importValue, modelUrl, cwd) {
76
+ const importUrlValue = getImportUrlValue(importValue)
77
+ if (!importUrlValue) {
78
+ return null
95
79
  }
80
+ const importIsObject = importValue && typeof importValue === 'object'
96
81
 
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']
82
+ if (isLocalJsImport(importUrlValue)) {
83
+ const modelDir = modelUrl && !isHttpUrl(modelUrl) ? path.dirname(modelUrl) : '.'
84
+ const localSchemaPath = path.normalize(path.join(modelDir, importUrlValue))
85
+ const schemaImport = localSchemaPath.split(path.sep).join('/')
86
+ return {
87
+ schemaImport: schemaImport,
88
+ schemaEntry: importIsObject ? { ...importValue, url: schemaImport } : schemaImport,
89
+ importUrl: toRuntimeUrl(schemaImport),
90
+ localFilePath: path.resolve(cwd, localSchemaPath),
91
+ remoteUrl: null
92
+ }
101
93
  }
102
94
 
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
- }
133
- }
134
- })
95
+ const remoteUrl = isHttpUrl(importUrlValue)
96
+ ? importUrlValue
97
+ : `https://cdn.jsdelivr.net/npm/${importUrlValue}`
98
+ return {
99
+ schemaImport: importUrlValue,
100
+ schemaEntry: importIsObject ? { ...importValue, url: importUrlValue } : importUrlValue,
101
+ importUrl: toRuntimeUrl(importUrlValue),
102
+ localFilePath: null,
103
+ remoteUrl: remoteUrl
135
104
  }
105
+ }
136
106
 
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)
107
+ function resolveRuntimeMode (runtime, fetchEnabled, outputs) {
108
+ const requestedRuntime = runtime || 'auto'
109
+ const availableModes = ['auto', 'local', 'cdn', 'inline']
110
+ if (!availableModes.includes(requestedRuntime)) {
111
+ throw new Error(`Invalid runtime mode: ${requestedRuntime}. Use one of: ${availableModes.join(', ')}`)
150
112
  }
151
- if (!Array.isArray(schema.model)) {
152
- schema.model = [schema.model]
113
+ if (requestedRuntime !== 'auto') {
114
+ return requestedRuntime
153
115
  }
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
161
- })
116
+ if (fetchEnabled) {
117
+ return 'inline'
162
118
  }
163
- console.log('Schema:', schema)
164
-
165
-
166
- // Generate description block
167
- if (description) {
168
- const descriptionMd = fs.readFileSync(path.join(cwd, description), 'utf8')
169
- descriptionHtml = converter.makeHtml(descriptionMd)
119
+ return outputs ? 'cdn' : 'local'
120
+ }
170
121
 
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
- }
122
+ function resolveOutputPath (cwd, outputPath) {
123
+ if (path.isAbsolute(outputPath)) {
124
+ return outputPath
180
125
  }
126
+ return path.join(cwd, outputPath)
127
+ }
181
128
 
182
- descriptionHtml += genHtmlFromSchema(schema)
183
-
184
-
185
-
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>`
212
- }
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
-
235
- if (ageInDays < 1) {
236
- console.log('Using cached import:', importUrl)
237
- importCode = fs.readFileSync(cacheFilePath, 'utf8');
238
- useCache = true;
239
- }
240
- }
241
-
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
- }
255
- }
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>`
129
+ async function loadRuntimeCode (version) {
130
+ if (version === 'dev') {
131
+ return fs.readFileSync(path.join(__dirname, '..', 'dist', 'jsee.js'), 'utf8')
261
132
  }
133
+ if (version === 'latest') {
134
+ return fs.readFileSync(path.join(__dirname, '..', 'dist', 'jsee.runtime.js'), 'utf8')
135
+ }
136
+ const response = await fetch(`https://cdn.jsdelivr.net/npm/@jseeio/jsee@${version}/dist/jsee.runtime.js`)
137
+ return response.text()
138
+ }
262
139
 
263
- let socialHtml = ''
264
- let gaHtml = ''
265
- let orgHtml = ''
266
-
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
- }
283
-
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>`
140
+ function getDataFromArgv (schema, argv, loadFiles=true) {
141
+ let data = {}
142
+ if (schema.inputs) {
143
+ schema.inputs.forEach(inp => {
144
+ const inputName = sanitizeName(inp.name)
145
+ console.log('Processing input:', inp.name, 'as', inputName)
146
+ if (inputName in argv) {
147
+ switch (inp.type) {
148
+ case 'file':
149
+ if (!loadFiles) {
150
+ // If we don't want to load files, just set the value to the file path
151
+ data[inp.name] = argv[inputName]
152
+ break
153
+ } else if (fs.existsSync(argv[inputName])) {
154
+ data[inp.name] = fs.readFileSync(argv[inputName], 'utf8')
155
+ } else {
156
+ console.error(`File not found: ${argv[inputName]}`)
157
+ process.exit(1)
158
+ }
300
159
  break
301
- case 'instagram':
302
- socialHtml += `<li><a rel="me" href="https://www.instagram.com/${url}">Instagram</a></li>`
160
+ case 'int':
161
+ data[inp.name] = parseInt(argv[inputName], 10)
303
162
  break
304
- case 'youtube':
305
- socialHtml += `<li><a rel="me" href="https://www.youtube.com/${url}">YouTube</a></li>`
163
+ case 'float':
164
+ data[inp.name] = parseFloat(argv[inputName])
306
165
  break
166
+ case 'string':
307
167
  default:
308
- socialHtml += `<li><a rel="me" href="${s.url}">${s.name}</a></li>`
168
+ data[inp.name] = argv[inputName]
309
169
  }
310
170
  }
311
- }
312
-
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
- }
320
-
321
- }
322
-
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
- })
332
-
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
171
  })
389
172
  }
173
+ return data
390
174
  }
391
175
 
392
176
  function genSchema (jsdocData) {
@@ -640,7 +424,7 @@ function template(schema, blocks) {
640
424
  pre { padding: 8px 12px; overflow-x: auto; }
641
425
  pre > code { border: 0; padding-right: 0; padding-left: 0; }
642
426
  .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; } }
427
+ @media screen and (min-width: 800px) { .wrapper { max-width: calc(1024px - (30px * 2)); padding-right: 30px; padding-left: 30px; } }
644
428
  .wrapper:after { content: ""; display: table; clear: both; }
645
429
  .orange { color: #f66a0a; }
646
430
  .grey { color: #828282; }
@@ -783,4 +567,481 @@ function template(schema, blocks) {
783
567
  </html>`
784
568
  }
785
569
 
786
- module.exports = gen
570
+ async function gen (pargv, returnHtml=false) {
571
+ // Determine if JSEE CLI is imported or run directly
572
+ const imported = path.dirname(__dirname) !== path.dirname(require.main.path)
573
+
574
+ // First pass over CLI arguments
575
+ // JSEE-level args
576
+ const argvAlias = {
577
+ inputs: 'i',
578
+ outputs: 'o',
579
+ description: 'd',
580
+ port: 'p',
581
+ version: 'v',
582
+ fetch: 'f',
583
+ execute: 'e',
584
+ cdn: 'c',
585
+ runtime: 'r',
586
+ }
587
+ const argvDefault = {
588
+ execute: false, // execute the model code on the server
589
+ fetch: false, // fetch the JSEE runtime from the CDN or local server
590
+ inputs: 'schema.json', // default input is schema.json in the current working directory
591
+ port: 3000, // default port for the server
592
+ version: 'latest', // default version of JSEE runtime to use
593
+ verbose: false, // verbose mode
594
+ cdn: false,
595
+ runtime: 'auto'
596
+ }
597
+ let argv = minimist(pargv, {
598
+ alias: argvAlias,
599
+ default: argvDefault,
600
+ boolean: ['help', 'h'],
601
+ })
602
+
603
+ if (argv.help || argv.h) {
604
+ console.log(`
605
+ Usage: jsee [schema.json] [options]
606
+
607
+ Options:
608
+ -i, --inputs <file> Input schema file (default: schema.json)
609
+ -o, --outputs <file> Output HTML file path
610
+ -d, --description <file> Markdown description file to include
611
+ -p, --port <number> Dev server port (default: 3000)
612
+ -v, --version <version> JSEE runtime version (default: latest)
613
+ -f, --fetch Fetch and bundle runtime + dependencies into output
614
+ -e, --execute Execute model server-side
615
+ -c, --cdn <url|bool> Rewrite model URLs for CDN deployment
616
+ -r, --runtime <mode> Runtime mode: auto|local|cdn|inline (default: auto)
617
+ --verbose Enable verbose logging
618
+
619
+ Examples:
620
+ jsee schema.json Start dev server with schema
621
+ jsee schema.json -o app.html Generate static HTML file
622
+ jsee schema.json -o app.html -f Generate self-contained HTML with bundled runtime
623
+ jsee -p 8080 Start dev server on port 8080
624
+
625
+ Documentation: https://jsee.org
626
+ `.trim())
627
+ return
628
+ }
629
+
630
+ // Set argv.inputs to the first non-option argument if it exists
631
+ if (!imported && argv._.length > 0 && !argv.inputs) {
632
+ argv.inputs = argv._[0]
633
+ }
634
+
635
+ function log (...args) {
636
+ if (argv.verbose) {
637
+ console.log('[JSEE CLI]', ...args)
638
+ }
639
+ }
640
+
641
+ log('Imported:', imported)
642
+ log('Current working directory:', process.cwd())
643
+ log('Script location:', __dirname)
644
+ log('Script file:', __filename)
645
+ log('Require location:', require.main.path)
646
+ log('Require file:', require.main.filename)
647
+
648
+ let cwd = process.cwd()
649
+ let inputs = argv.inputs
650
+ let outputs = argv.outputs
651
+ let description = argv.description
652
+ let schema
653
+ let schemaPath
654
+ let descriptionTxt = ''
655
+ let descriptionHtml = ''
656
+ let jsdocMarkdown = ''
657
+ let modelFuncs = {}
658
+
659
+ // Determine the inputs and outputs
660
+ // if inputs is a string with js file names, split it into an array
661
+ if (typeof inputs === 'string') {
662
+ if (inputs.includes('.js')) {
663
+ inputs = inputs.split(',')
664
+ }
665
+ }
666
+
667
+ // if outputs is a string with js file names, split it into an array
668
+ if (typeof outputs === 'string') {
669
+ outputs = outputs.split(',')
670
+ }
671
+
672
+ if (inputs.length === 0) {
673
+ console.error('No inputs provided')
674
+ process.exit(1)
675
+ } else if ((inputs.length === 1) && (inputs[0].includes('.json'))) {
676
+ // Input is json schema
677
+ // Curren working directory if not provided
678
+ // schema = require(path.join(cwd, inputs[0]))
679
+ // switch to fs.readFileSync to reload the schema if it changes
680
+ schemaPath = inputs[0].startsWith('/') ? inputs[0] : path.join(cwd, inputs[0])
681
+ schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'))
682
+ } else {
683
+ // Array of js files
684
+ // Generate schema
685
+ let jsdocData = jsdoc2md.getTemplateDataSync({ files: inputs.map(f => path.join(cwd, f)) })
686
+ schema = genSchema(jsdocData)
687
+ // jsdocMarkdown = jsdoc2md.renderSync({
688
+ // data: jsdocData,
689
+ // 'param-list-format': 'list',
690
+ // })
691
+ }
692
+ log('Schema path:', schemaPath)
693
+
694
+ // Second pass over CLI arguments
695
+ // Iterate over schema inputs and update argv aliases
696
+ if (schema.inputs) {
697
+ schema.inputs.forEach((inp, inp_index) => {
698
+ if (inp.name) {
699
+ const inputName = sanitizeName(inp.name)
700
+ if (inp.alias) {
701
+ argvAlias[inputName] = inp.alias
702
+ }
703
+ // Use positional arguments as schema inputs defaults if JSEE CLI is imported
704
+ if (imported && argv._.length > inp_index) {
705
+ log('Using positional argument for input:', inputName, argv._[inp_index])
706
+ argvDefault[inputName] = argv._[inp_index]
707
+ }
708
+ // We don't need to duplicate defaults here, as we handle them on the frontend
709
+ // else if (inp.default) {
710
+ // argvDefault[inputName] = inp.default
711
+ // }
712
+ }
713
+ })
714
+ }
715
+ // Update argv with the new aliases and defaults
716
+ argv = minimist(pargv, {
717
+ alias: argvAlias,
718
+ default: argvDefault,
719
+ })
720
+
721
+ // Now deactivate the inputs present in argv
722
+ // If you set parameter on the command line, it should not be editable in the GUI
723
+ // E.g. file selected
724
+ const dataFromArgvWithoutFileLoading = getDataFromArgv(schema, argv, false)
725
+ log('Data from argv without file loading:', dataFromArgvWithoutFileLoading)
726
+ if (schema.inputs) {
727
+ schema.inputs.forEach(inp => {
728
+ // Here data contains unsanitized input names
729
+ if (inp.name in dataFromArgvWithoutFileLoading) {
730
+ inp.default = dataFromArgvWithoutFileLoading[inp.name]
731
+ inp.disabled = true // Deactivate the input if it's present in argv
732
+ }
733
+ })
734
+ }
735
+ log('Argv:', argv)
736
+
737
+ // Initially in argv.fetch branch
738
+ // Check if schema has model, convert to array if needed
739
+ if (!schema.model) {
740
+ // console.error('No model found in schema')
741
+ // process.exit(1)
742
+ // It's still valid schema, can be only render function or vis of inputs/outputs
743
+ schema.model = []
744
+ }
745
+ if (!Array.isArray(schema.model)) {
746
+ schema.model = [schema.model]
747
+ }
748
+
749
+ // Server-side execution
750
+ // If execute is true, we will prepare the model functions to run on the server side
751
+ // Schema model will be updated with the server url and POST method
752
+ if (argv.execute) {
753
+ await Promise.all(schema.model.map(async m => {
754
+ log('Preparing a model to run on the server side:', m.name, m.url)
755
+ const target = require(path.join(schemaPath ? path.dirname(schemaPath) : cwd, m.url))
756
+ modelFuncs[m.name] = await getModelFuncJS(m, target, {log})
757
+ m.type = 'post'
758
+ m.url = `/${m.name}`
759
+ m.worker = false
760
+ }))
761
+ }
762
+
763
+ // Switch to CDN for model files
764
+ if (argv.cdn) {
765
+ let cdn = ''
766
+ console.log(argv)
767
+ if (typeof argv.cdn === 'string') {
768
+ cdn = argv.cdn
769
+ } else if (typeof argv.cdn === 'boolean') {
770
+ // Check package.json in cwd
771
+ const packageJsonPath = path.join(cwd, 'package.json')
772
+ if (fs.existsSync(packageJsonPath)) {
773
+ const target = require(packageJsonPath)
774
+ const packageName = target.name
775
+ cdn = `https://cdn.jsdelivr.net/npm/${packageName}@${target.version}/`
776
+ } else {
777
+ console.error(`No package.json found: ${packageJsonPath}`)
778
+ process.exit(1)
779
+ }
780
+ } else {
781
+ console.error('Invalid CDN argument. Use --cdn <url> or --cdn true to use package.json version.')
782
+ process.exit(1)
783
+ }
784
+ log('Using CDN for model files:', cdn)
785
+ schema.model.forEach(m => {
786
+ if (m.url) {
787
+ // If url is relative, make it absolute
788
+ if (!m.url.startsWith('http')) {
789
+ m.url = path.join(cdn, m.url)
790
+ log(`Updated ${m.name} model URL to: ${m.url}`)
791
+ }
792
+ }
793
+ })
794
+ }
795
+
796
+ log('Schema:', schema)
797
+
798
+ // Generate description block
799
+ if (description) {
800
+ const descriptionMd = fs.readFileSync(path.join(cwd, description), 'utf8')
801
+ descriptionHtml = converter.makeHtml(descriptionMd)
802
+
803
+ if (descriptionMd.includes('---')) {
804
+ descriptionTxt = descriptionMd
805
+ .split('---')[0]
806
+ .replace(/\n/g, ' ')
807
+ .replace(/\s+/g, ' ')
808
+ .replace(/#/g, '')
809
+ .replace(/\*/g, '')
810
+ .trim()
811
+ }
812
+ }
813
+ descriptionHtml += genHtmlFromSchema(schema)
814
+
815
+ // Generate jsee code
816
+ let jseeHtml = ''
817
+ let hiddenElementHtml = ''
818
+ const hasOutputs = Array.isArray(outputs) ? outputs.length > 0 : Boolean(outputs)
819
+ const runtimeMode = resolveRuntimeMode(argv.runtime, argv.fetch, hasOutputs)
820
+ if (argv.fetch) {
821
+ // Fetch jsee code from the CDN or local server
822
+ const jseeCode = await loadRuntimeCode(argv.version)
823
+ jseeHtml = `<script>${jseeCode}</script>`
824
+ // Fetch model files and store them in hidden elements
825
+ hiddenElementHtml += '<div id="hidden-storage" style="display: none;">'
826
+
827
+ const bundleBlocks = collectFetchBundleBlocks(schema)
828
+ for (let m of bundleBlocks) {
829
+ if (m.type === 'get' || m.type === 'post') {
830
+ continue
831
+ }
832
+ if (m.url) {
833
+ // Fetch model from the local file system (remote URLs not yet supported here)
834
+ const modelCode = fs.readFileSync(path.join(cwd, m.url), 'utf8')
835
+ hiddenElementHtml += `<script type="text/plain" style="display: none;" data-src="${m.url}">${modelCode}</script>`
836
+ }
837
+ const imports = toArray(m.imports)
838
+ if (imports.length) {
839
+ m.imports = imports
840
+ for (let [index, i] of imports.entries()) {
841
+ if (typeof i !== 'string') {
842
+ continue
843
+ }
844
+ const importMeta = resolveFetchImport(i, m.url, cwd)
845
+ if (!importMeta) {
846
+ continue
847
+ }
848
+ m.imports[index] = importMeta.schemaEntry
849
+
850
+ let importCode
851
+ if (importMeta.localFilePath) {
852
+ importCode = fs.readFileSync(importMeta.localFilePath, 'utf8')
853
+ } else {
854
+ // Create cache directory if it doesn't exist
855
+ const cacheDir = path.join(os.homedir(), '.cache', 'jsee')
856
+ fs.mkdirSync(cacheDir, { recursive: true })
857
+
858
+ // Create a hash of the importUrl
859
+ const hash = crypto.createHash('sha256').update(importMeta.importUrl).digest('hex')
860
+ const cacheFilePath = path.join(cacheDir, `${hash}.js`)
861
+
862
+ let useCache = false
863
+
864
+ // Check if cache file exists and is less than 1 day old
865
+ if (fs.existsSync(cacheFilePath)) {
866
+ const stats = fs.statSync(cacheFilePath)
867
+ const mtime = new Date(stats.mtime)
868
+ const now = new Date()
869
+ const ageInDays = (now - mtime) / (1000 * 60 * 60 * 24)
870
+
871
+ if (ageInDays < 1) {
872
+ log('Using cached import:', importMeta.importUrl)
873
+ importCode = fs.readFileSync(cacheFilePath, 'utf8')
874
+ useCache = true
875
+ }
876
+ }
877
+
878
+ if (!useCache) {
879
+ const response = await fetch(importMeta.remoteUrl)
880
+ if (!response.ok) {
881
+ console.error(`Failed to fetch ${importMeta.remoteUrl}: ${response.statusText}`)
882
+ process.exit(1)
883
+ }
884
+ importCode = await response.text()
885
+ fs.writeFileSync(cacheFilePath, importCode, 'utf8')
886
+ log('Fetched and stored to cache:', importMeta.importUrl)
887
+ }
888
+ }
889
+ hiddenElementHtml += `<script type="text/plain" style="display: none;" data-src="${importMeta.importUrl}">${importCode}</script>`
890
+ }
891
+ }
892
+ }
893
+ hiddenElementHtml += '</div>'
894
+ } else {
895
+ if (runtimeMode === 'inline') {
896
+ const jseeCode = await loadRuntimeCode(argv.version)
897
+ jseeHtml = `<script>${jseeCode}</script>`
898
+ } else if (runtimeMode === 'cdn') {
899
+ jseeHtml = `<script src="https://cdn.jsdelivr.net/npm/@jseeio/jsee@${argv.version}/dist/jsee.runtime.js"></script>`
900
+ } else {
901
+ jseeHtml = argv.version === 'dev'
902
+ ? `<script src="http://localhost:${argv.port}/dist/jsee.js"></script>`
903
+ : `<script src="http://localhost:${argv.port}/dist/jsee.runtime.js"></script>`
904
+ }
905
+ }
906
+
907
+ let socialHtml = ''
908
+ let gaHtml = ''
909
+ let orgHtml = ''
910
+
911
+ if (schema.page) {
912
+ if (schema.page.ga) {
913
+ gaHtml = `
914
+ <script id="ga-src" async src="https://www.googletagmanager.com/gtag/js?id=${schema.page.ga}"></script>
915
+ <script id="ga-body">
916
+ window['ga-disable-${schema.page.ga}'] = window.doNotTrack === "1" || navigator.doNotTrack === "1" || navigator.doNotTrack === "yes" || navigator.msDoNotTrack === "1";
917
+ window.dataLayer = window.dataLayer || [];
918
+ function gtag(){dataLayer.push(arguments);}
919
+ gtag('js', new Date());
920
+ gtag('config', '${schema.page.ga}');
921
+ </script>
922
+ `
923
+ }
924
+
925
+ // Social media links
926
+ if (schema.page.social) {
927
+ // iterate over dict with k, v pairs
928
+ for (let [name, url] of Object.entries(schema.page.social)) {
929
+ switch (name) {
930
+ case 'twitter':
931
+ socialHtml += `<li><a rel="me" href="https://twitter.com/${url}">Twitter</a></li>`
932
+ break
933
+ case 'github':
934
+ socialHtml += `<li><a rel="me" href="https://github.com/${url}">GitHub</a></li>`
935
+ break
936
+ case 'facebook':
937
+ socialHtml += `<li><a rel="me" href="https://www.facebook.com/${url}">Facebook</a></li>`
938
+ break
939
+ case 'linkedin':
940
+ socialHtml += `<li><a rel="me" href="https://www.linkedin.com/company/${url}">LinkedIn</a></li>`
941
+ break
942
+ case 'instagram':
943
+ socialHtml += `<li><a rel="me" href="https://www.instagram.com/${url}">Instagram</a></li>`
944
+ break
945
+ case 'youtube':
946
+ socialHtml += `<li><a rel="me" href="https://www.youtube.com/${url}">YouTube</a></li>`
947
+ break
948
+ default:
949
+ socialHtml += `<li><a rel="me" href="${url}">${name}</a></li>`
950
+ }
951
+ }
952
+ }
953
+
954
+ if (schema.page.org) {
955
+ orgHtml = `<div class="footer-org"><h4 class="footer-heading"><a href="${schema.page.org.url}">${schema.page.org.name}</a></h4>`
956
+ if (schema.page.org.description) {
957
+ orgHtml += `<p>${schema.page.org.description}</p>`
958
+ }
959
+ orgHtml += '</div>'
960
+ }
961
+
962
+ }
963
+
964
+ const html = template(schema, {
965
+ descriptionHtml: pad(descriptionHtml, 8, 1),
966
+ descriptionTxt: descriptionTxt,
967
+ gaHtml: pad(gaHtml, 2, 1),
968
+ jseeHtml: jseeHtml,
969
+ hiddenElementHtml: hiddenElementHtml,
970
+ socialHtml: pad(socialHtml, 2, 1),
971
+ orgHtml: pad(orgHtml, 2, 1),
972
+ })
973
+
974
+ if (returnHtml) {
975
+ // Return the html as a string
976
+ return html
977
+ } else if (outputs) {
978
+ // Store the html in the output file
979
+ for (let o of outputs) {
980
+ if (o === 'stdout') {
981
+ log(html)
982
+ } else if (o.includes('.html')) {
983
+ fs.writeFileSync(resolveOutputPath(cwd, o), html)
984
+ } else if (o.includes('.json')) {
985
+ fs.writeFileSync(resolveOutputPath(cwd, o), JSON.stringify(schema, null, 2))
986
+ } else if (o.includes('.md')) {
987
+ fs.writeFileSync(resolveOutputPath(cwd, o), genMarkdownFromSchema(schema))
988
+ } else {
989
+ console.error('Invalid output file:', o)
990
+ }
991
+ }
992
+ } else {
993
+ // Serve the html
994
+ const express = require('express')
995
+ const app = express()
996
+ app.use(express.json())
997
+ if (argv.execute) {
998
+ // Create post endpoint for executing the model
999
+ schema.model.forEach(m => {
1000
+ app.post(m.url, (req, res) => {
1001
+ log(`Executing model: ${m.name}`)
1002
+ if (m.name in modelFuncs) {
1003
+ const modelFunc = modelFuncs[m.name]
1004
+ try {
1005
+ const dataFromArgv = getDataFromArgv(schema, argv)
1006
+ const dataFromGUI = req.body
1007
+ const data = { ...dataFromGUI, ...dataFromArgv }
1008
+ log('Data for model execution:', data)
1009
+ const result = modelFunc(data)
1010
+ res.json(result)
1011
+ log(`Model ${m.name} executed successfully: `, result)
1012
+ } catch (error) {
1013
+ console.error('Error executing model:', error)
1014
+ res.status(500).json({ error: error.message })
1015
+ }
1016
+ }
1017
+ })
1018
+ log('Model execution endpoints created:', m.url)
1019
+ })
1020
+ }
1021
+ app.get('/', async (req, res) => {
1022
+ log('Serving index.html')
1023
+ res.send(await gen(pargv, true))
1024
+ })
1025
+ // app.get('/dist/jsee.runtime.js', (req, res) => {
1026
+ // // __dirname points to this file location (it's jsee/src/cli.js, likely in node_modules)
1027
+ // // so we need to go up one level to get to the dist folder with jsee.runtime.js
1028
+ // const pathToJSEE = path.join(__dirname, '..', 'dist', 'jsee.runtime.js')
1029
+ // log(`Serving jsee.runtime.js from: ${pathToJSEE}`)
1030
+ // res.sendFile(pathToJSEE)
1031
+ // })
1032
+ app.use('/dist', express.static(path.join(__dirname, '..', 'dist'))) // Serve static files from the dist folder
1033
+ // app.use(express.static(cwd))
1034
+ // app.use(express.static(require.main.path)) // Serve static files from the main module path
1035
+ // app.use(express.static(path.join(require.main.path, '..'))) // Serve static files from the parent directory of the main module path
1036
+ app.use(express.static(schemaPath ? path.dirname(schemaPath) : cwd)) // Serve static files from the schema path or current working directory
1037
+ app.listen(argv.port, () => {
1038
+ console.log(`JSEE app is running: http://localhost:${argv.port}`)
1039
+ })
1040
+ }
1041
+ }
1042
+
1043
+ module.exports = gen
1044
+ module.exports.collectFetchBundleBlocks = collectFetchBundleBlocks
1045
+ module.exports.resolveFetchImport = resolveFetchImport
1046
+ module.exports.resolveRuntimeMode = resolveRuntimeMode
1047
+ module.exports.resolveOutputPath = resolveOutputPath