@jseeio/jsee 0.3.0 → 0.3.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jseeio/jsee",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "JavaScript Execution Environment",
5
5
  "main": "dist/jsee.js",
6
6
  "unpkg": "dist/jsee.js",
@@ -10,12 +10,15 @@
10
10
  "build-dev": "webpack --mode=development --progress --stats-children --env DEVELOPMENT",
11
11
  "build": "webpack --mode=production --progress && webpack --mode=production --progress --env RUNTIME && npm test",
12
12
  "watch": "nodemon --watch . --ignore dist,.git --ext vue,js,css,html --exec 'npm run build-dev && npm run test:basic'",
13
- "prepublishOnly": "npm run build && npm test",
13
+ "prepublishOnly": "npm run build",
14
14
  "test": "npm run test:basic && npm run test:python",
15
15
  "test:basic": "jest test/test-basic.test.js --detectOpenHandles",
16
16
  "test:python": "jest test/test-python.test.js --detectOpenHandles",
17
17
  "test-head": "HEADLESS=false npm test"
18
18
  },
19
+ "bin": {
20
+ "jsee": "./bin/jsee"
21
+ },
19
22
  "author": "Anton Zemlyansky",
20
23
  "license": "MIT",
21
24
  "repository": {
@@ -31,9 +34,13 @@
31
34
  "bulma": "^0.9.3",
32
35
  "csv-parse": "^4.6.1",
33
36
  "element-plus": "^1.3.0-beta.1",
37
+ "express": "^4.19.2",
34
38
  "file-saver": "^2.0.2",
35
39
  "filtrex": "^2.2.3",
40
+ "jsdoc-to-markdown": "^8.0.1",
41
+ "minimist": "^1.2.8",
36
42
  "notyf": "^3.10.0",
43
+ "showdown": "^2.1.0",
37
44
  "vue": "^3.2.47",
38
45
  "vue-style-loader": "^4.1.3",
39
46
  "vue3-json-viewer": "^2.2.2",
package/src/main.js CHANGED
@@ -136,6 +136,7 @@ export default class JSEE {
136
136
  verbose = !(params.verbose === false)
137
137
  this.container = params.container
138
138
  this.schema = params.schema || params.config // Previous naming
139
+ this.utils = utils
139
140
  this.__version__ = VERSION
140
141
 
141
142
  // Check if schema is provided
@@ -196,8 +197,20 @@ export default class JSEE {
196
197
  // Check if schema is a string (url to json)
197
198
  if (typeof this.schema === 'string') {
198
199
  this.schemaUrl = this.schema.indexOf('json') ? this.schema : this.schema + '.json'
199
- this.schema = await fetch(this.schemaUrl)
200
- this.schema = await this.schema.json()
200
+
201
+ // Check if schema is present in the hidden DOM element
202
+ const schema = utils.loadFromDOM(this.schemaUrl)
203
+ if (schema) {
204
+ // Schema block found in the hidden element, use its content
205
+ this.schema = JSON.parse(schema);
206
+ log(`Loaded schema from the hidden DOM element for ${this.schemaUrl}:`, this.schema);
207
+ } else {
208
+ // Fetch schema from the URL
209
+ log('Fetching schema from:', this.schemaUrl)
210
+ this.schema = await fetch(this.schemaUrl)
211
+ this.schema = await this.schema.json()
212
+ log('Loaded schema from URL:', this.schema)
213
+ }
201
214
  }
202
215
 
203
216
  // Check if schema is a function (model)
@@ -264,14 +277,22 @@ export default class JSEE {
264
277
 
265
278
  // Load code if url is provided
266
279
  if (m.url && (m.url.includes('.js') || m.url.includes('.py'))) {
267
- // Update model URL if needed
268
- if (!m.url.includes('/') && this.schemaUrl && this.schemaUrl.includes('/')) {
269
- m.url = window.location.protocol + '//' + window.location.host + this.schemaUrl.split('/').slice(0, -1).join('/') + '/' + m.url
270
- log(`Changed the old model URL to ${m.url} (based on the schema URL)`)
280
+ // Try to get the code from a hidden DOM element first
281
+ const modelCode = utils.loadFromDOM(m.url)
282
+ if (modelCode) {
283
+ // Code block found in the hidden element, use its content
284
+ m.code = modelCode
285
+ log(`Loaded code from the hidden DOM element for ${m.url}`);
286
+ } else {
287
+ // Update model URL if needed
288
+ if (!m.url.includes('/') && this.schemaUrl && this.schemaUrl.includes('/')) {
289
+ m.url = window.location.protocol + '//' + window.location.host + this.schemaUrl.split('/').slice(0, -1).join('/') + '/' + m.url
290
+ log(`Changed the old model URL to ${m.url} (based on the schema URL)`)
291
+ }
292
+ log('Loaded code from:', m.url)
293
+ m.code = await fetch(m.url)
294
+ m.code = await m.code.text()
271
295
  }
272
- log('Loaded code from:', m.url)
273
- m.code = await fetch(m.url)
274
- m.code = await m.code.text()
275
296
  }
276
297
 
277
298
  // Update model name if absent
@@ -295,10 +316,26 @@ export default class JSEE {
295
316
  m.type = getModelType(m)
296
317
  }
297
318
 
298
-
319
+ // Load imports from hidden DOM element
320
+ if (m.imports && Array.isArray(m.imports) && m.imports.length) {
321
+ for (let [i, imp] of m.imports.entries()) {
322
+ if (typeof imp === 'string') {
323
+ // Convert string to object
324
+ m.imports[i] = {
325
+ url: imp
326
+ }
327
+ imp = m.imports[i]
328
+ }
329
+ if (!m.type.includes('py')) {
330
+ imp.url = utils.getUrl(imp.url)
331
+ imp.code = utils.loadFromDOM(imp.url)
332
+ }
333
+ }
334
+ }
335
+ console.log('Imports:', m.imports)
299
336
  } // end of model-loop
300
337
 
301
- log('Model initialized, size:', this.model.length)
338
+ log('Models initialized:', this.model.length)
302
339
  }
303
340
 
304
341
  async initInputs () {
@@ -473,7 +510,7 @@ export default class JSEE {
473
510
  await utils.importScripts(['https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js'])
474
511
  const pyodide = await loadPyodide()
475
512
  if (model.imports && Array.isArray(model.imports) && model.imports.length) {
476
- await pyodide.loadPackage(model.imports)
513
+ await pyodide.loadPackage(model.imports.url)
477
514
  } else {
478
515
  await pyodide.loadPackagesFromImports(model.code)
479
516
  }
@@ -532,7 +569,7 @@ export default class JSEE {
532
569
  this.worker.postMessage(this.schema.model)
533
570
  } else {
534
571
  // Main:
535
- this.modelFunc = utils.getModelFuncAPI(model, log)
572
+ return utils.getModelFuncAPI(this.schema.model, log)
536
573
  }
537
574
  }
538
575
 
@@ -675,4 +712,94 @@ export default class JSEE {
675
712
  }]
676
713
  }
677
714
  }
715
+
716
+ async download (title='output') {
717
+ // Cache the model
718
+ const clone = document.cloneNode(true)
719
+
720
+ // Change #download-btn to 'Offline: version'
721
+ const downloadBtn = clone.getElementById('download-btn')
722
+ downloadBtn.textContent = 'Offline: latest'
723
+ downloadBtn.disabled = true
724
+ downloadBtn.style.cursor = 'not-allowed'
725
+
726
+ let hiddenElement = clone.getElementById('hidden-storage');
727
+ if (!hiddenElement) {
728
+ hiddenElement = clone.createElement('div');
729
+ hiddenElement.style.display = 'none'; // Make it hidden
730
+ hiddenElement.id = 'hidden-storage'; // Assign an ID
731
+ clone.body.prepend(hiddenElement)
732
+ }
733
+
734
+ function storeInHiddenElement (url, value) {
735
+ const element = clone.createElement('script')
736
+ element.type = 'text/plain' // Make it non-executable
737
+ element.style.display = 'none' // Make it hidden
738
+ element.setAttribute('data-src', url) // Use data attribute for key
739
+ element.textContent = typeof value === 'object' ? JSON.stringify(value) : value
740
+ hiddenElement.appendChild(element)
741
+ console.log('[Hidden store] Stored:', url)
742
+ }
743
+
744
+ // Remove Google Analytics script tags
745
+ try {
746
+ clone.getElementById('ga-src').remove()
747
+ clone.getElementById('ga-body').remove()
748
+ } catch (error) {
749
+ console.error('Error removing GA script tags:', error.message)
750
+ }
751
+
752
+ console.log('Caching schema:', env.schema)
753
+ storeInHiddenElement(env.schemaUrl, env.schema)
754
+
755
+ console.log('Caching models:', env.model)
756
+ for (const model of env.model) {
757
+ storeInHiddenElement(model.url, model.code)
758
+ // Iterate over imports
759
+ if (model.imports) {
760
+ for (let imp of model.imports) {
761
+ // Store the import
762
+ const response = await fetch(imp.url)
763
+ const content = await response.text()
764
+ storeInHiddenElement(imp.url, content)
765
+ // Remove any src-based script tags with the same URL
766
+ const script = clone.querySelector('script[src="' + imp.url + '"]')
767
+ if (script) {
768
+ script.remove()
769
+ }
770
+ }
771
+ }
772
+ }
773
+
774
+ // append dummy src script for webpack fix
775
+ // const dummyScript = document.createElement('script')
776
+ // dummyScript.src = 'https://example.com/dummy.js'
777
+ // clone.body.appendChild(dummyScript)
778
+
779
+ // Find all external script tags and replace them with inline script tags
780
+ const externalScripts = Array.from(clone.querySelectorAll('script[src]'))
781
+ for (const script of externalScripts) {
782
+ try {
783
+ const response = await fetch(script.src);
784
+ if (!response.ok) throw new Error('Network response was not ok for script:' + script.src);
785
+ const content = await response.text()
786
+ const inlineScript = document.createElement('script')
787
+ inlineScript.textContent = content
788
+ inlineScript.setAttribute('data-src', script.src)
789
+ script.parentNode.replaceChild(inlineScript, script)
790
+ } catch (error) {
791
+ console.error("Error fetching script:", error.message);
792
+ }
793
+ }
794
+ // Prepare the HTML for download and trigger the download
795
+ const html = '<!DOCTYPE html>\n' + clone.documentElement.outerHTML
796
+ console.log(html)
797
+ const blob = new Blob([html], { type: 'text/html' })
798
+ const url = URL.createObjectURL(blob)
799
+ const a = document.createElement('a')
800
+ a.href = url
801
+ a.download = title + '.html'
802
+ a.click()
803
+ URL.revokeObjectURL(url)
804
+ }
678
805
  }
package/src/utils.js CHANGED
@@ -57,24 +57,49 @@ function getUrl (url) {
57
57
  return newUrl
58
58
  }
59
59
 
60
- function importScriptAsync (url, async=true) {
61
- url = getUrl(url)
60
+ function loadFromDOM (url) {
61
+ const scriptElement = document.querySelector(`script[data-src="${url}"]`)
62
+ if (scriptElement) {
63
+ return scriptElement.textContent
64
+ } else {
65
+ return null
66
+ }
67
+ }
68
+
69
+ function importScriptAsync (imp, async=true) {
62
70
  return new Promise((resolve, reject) => {
63
71
  try {
64
72
  const scriptElement = document.createElement('script')
65
73
  scriptElement.type = 'text/javascript'
66
- scriptElement.async = async
67
- scriptElement.src = url
68
- scriptElement.addEventListener('load', (ev) => {
69
- resolve({ status: true })
70
- })
71
- scriptElement.addEventListener('error', (ev) => {
72
- reject({
73
- status: false,
74
- message: `Failed to import ${url}`
74
+ if (imp.code) {
75
+ // Create script element from import.code
76
+ scriptElement.textContent = imp.code
77
+ // Create event element to notify about script load
78
+ const eventElement = document.createElement('script')
79
+ eventElement.type = 'text/javascript'
80
+ eventElement.textContent = `document.dispatchEvent(new CustomEvent('${imp.url}', {detail: {url: '${imp.url}'}}));`
81
+ document.addEventListener(imp.url, (ev) => {
82
+ console.log('Script loaded from cache:', ev.detail.url)
83
+ resolve({ status: true })
75
84
  })
76
- })
77
- document.body.appendChild(scriptElement);
85
+ document.body.appendChild(scriptElement);console.log('1')
86
+ document.body.appendChild(eventElement)
87
+ } else {
88
+ // Create script element from import.url
89
+ scriptElement.async = async
90
+ scriptElement.src = imp.url
91
+ scriptElement.addEventListener('load', (ev) => {
92
+ console.log('Script loaded:', imp.url)
93
+ resolve({ status: true })
94
+ })
95
+ scriptElement.addEventListener('error', (ev) => {
96
+ reject({
97
+ status: false,
98
+ message: `Failed to import ${imp.url}`
99
+ })
100
+ })
101
+ document.body.appendChild(scriptElement);
102
+ }
78
103
  } catch (error) {
79
104
  reject(error)
80
105
  }
@@ -85,8 +110,8 @@ async function importScripts (...imports) {
85
110
  // Load scripts in parallel
86
111
  // return Promise.all(imports.map(importScriptAsync))
87
112
  // Load scripts in sequence. Possible ordering issues.
88
- for (const scriptUrl of imports) {
89
- await importScriptAsync(scriptUrl);
113
+ for (const imp of imports) {
114
+ await importScriptAsync(imp);
90
115
  }
91
116
  }
92
117
 
@@ -103,8 +128,8 @@ function getModelFuncAPI (model, log=console.log) {
103
128
  }
104
129
  case 'post':
105
130
  return (data) => {
106
- log('Sending POST request to', this.schema.model.url)
107
- const resPromise = fetch(this.schema.model.url, {
131
+ log('Sending POST request to', model.url)
132
+ const resPromise = fetch(model.url, {
108
133
  method: 'POST',
109
134
  headers: {
110
135
  'Accept': 'application/json',
@@ -123,6 +148,7 @@ async function delay (ms) {
123
148
 
124
149
  module.exports = {
125
150
  isObject,
151
+ loadFromDOM,
126
152
  getModelFuncJS,
127
153
  getModelFuncAPI,
128
154
  importScripts,
package/src/worker.js CHANGED
@@ -18,7 +18,7 @@ async function initPython (model) {
18
18
  importScripts("https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js")
19
19
  const pyodide = await loadPyodide()
20
20
  if (model.imports && Array.isArray(model.imports) && model.imports.length) {
21
- await pyodide.loadPackage(model.imports)
21
+ await pyodide.loadPackage(model.imports.map(i => i.url))
22
22
  } else {
23
23
  await pyodide.loadPackagesFromImports(model.code)
24
24
  }
@@ -39,9 +39,13 @@ async function initJS (model) {
39
39
  log('Loading imports...')
40
40
  for (let imp of model.imports) {
41
41
  // Try creating an url
42
- const url = utils.getUrl(imp)
43
- log('Importing:', url)
44
- importScripts(url)
42
+ if (imp.code) {
43
+ log('Importing from DOM:', imp.url)
44
+ importScripts(URL.createObjectURL(new Blob([imp.code], { type: 'text/javascript' })))
45
+ } else {
46
+ log('Importing from network:', imp.url)
47
+ importScripts(imp.url)
48
+ }
45
49
  }
46
50
  }
47
51
 
package/webpack.config.js CHANGED
@@ -10,6 +10,7 @@ module.exports = (env) => {
10
10
  output: {
11
11
  filename: env.RUNTIME ? 'jsee.runtime.js' : 'jsee.js',
12
12
  path: path.resolve(__dirname, 'dist'),
13
+ publicPath: '/dist/', // Should fix Uncaught Error when downloaded: Automatic publicPath is not supported in this browser
13
14
  library: {
14
15
  type: 'umd',
15
16
  name: 'JSEE',