@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/CHANGELOG.md +4 -0
- package/bin/jsee +575 -0
- package/dist/jsee.js +1 -1
- package/dist/jsee.runtime.js +1 -1
- package/package.json +9 -2
- package/src/main.js +140 -13
- package/src/utils.js +43 -17
- package/src/worker.js +8 -4
- package/webpack.config.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jseeio/jsee",
|
|
3
|
-
"version": "0.3.
|
|
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
|
|
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
|
-
|
|
200
|
-
|
|
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
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
|
61
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
|
89
|
-
await importScriptAsync(
|
|
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',
|
|
107
|
-
const resPromise = fetch(
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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',
|