@jseeio/jsee 0.4.0 → 0.4.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.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "JavaScript Execution Environment",
5
5
  "main": "dist/jsee.js",
6
6
  "unpkg": "dist/jsee.js",
package/src/app.js CHANGED
@@ -231,7 +231,9 @@ function createVueApp (env, mountedCallback, logMain) {
231
231
  app.use(window[framework])
232
232
  }
233
233
 
234
- return app.mount(container) // After app.mount() it's not the same app
234
+ const component = app.mount(container) // After app.mount() it's not the same app
235
+ component.__vueApp = app
236
+ return component
235
237
  }
236
238
 
237
239
  export { createVueApp }
package/src/cli.js CHANGED
@@ -80,26 +80,58 @@ function getImportUrlValue (importValue) {
80
80
  return null
81
81
  }
82
82
 
83
+ // Resolve an import path to a local file by checking candidate locations on disk.
84
+ // Returns the absolute file path if found, null otherwise.
85
+ // This replaces heuristic prefix detection (isLocalJsImport/isLocalCssImport) with
86
+ // a definitive filesystem check — no heuristic can reliably distinguish bare-relative
87
+ // local paths like "dist/core.js" from npm package subpaths like "chart.js/dist/chart.min.js".
88
+ function resolveLocalImportFile (importUrlValue, modelUrl, cwd) {
89
+ if (!importUrlValue || typeof importUrlValue !== 'string') return null
90
+ if (isHttpUrl(importUrlValue)) return null
91
+
92
+ const modelDir = modelUrl && !isHttpUrl(modelUrl) ? path.dirname(modelUrl) : '.'
93
+ const candidates = []
94
+
95
+ if (importUrlValue.startsWith('./') || importUrlValue.startsWith('../')) {
96
+ // Explicit relative: prefer model-relative (colocated helpers), fallback to cwd
97
+ candidates.push(path.resolve(cwd, path.join(modelDir, importUrlValue)))
98
+ candidates.push(path.resolve(cwd, importUrlValue))
99
+ } else if (importUrlValue.startsWith('/')) {
100
+ // Absolute from project root
101
+ candidates.push(path.resolve(cwd, importUrlValue.slice(1)))
102
+ } else {
103
+ // Bare relative (dist/core.js): prefer cwd (schema-relative), fallback to model-relative
104
+ candidates.push(path.resolve(cwd, importUrlValue))
105
+ candidates.push(path.resolve(cwd, path.join(modelDir, importUrlValue)))
106
+ }
107
+
108
+ for (const fp of candidates) {
109
+ if (fs.existsSync(fp) && fs.statSync(fp).isFile()) return fp
110
+ }
111
+ return null
112
+ }
113
+
83
114
  function resolveFetchImport (importValue, modelUrl, cwd) {
84
115
  const importUrlValue = getImportUrlValue(importValue)
85
- if (!importUrlValue) {
86
- return null
87
- }
116
+ if (!importUrlValue) return null
88
117
  const importIsObject = importValue && typeof importValue === 'object'
89
118
 
90
- if (isLocalJsImport(importUrlValue) || isLocalCssImport(importUrlValue)) {
91
- const modelDir = modelUrl && !isHttpUrl(modelUrl) ? path.dirname(modelUrl) : '.'
92
- const localSchemaPath = path.normalize(path.join(modelDir, importUrlValue))
93
- const schemaImport = localSchemaPath.split(path.sep).join('/')
119
+ const localFilePath = resolveLocalImportFile(importUrlValue, modelUrl, cwd)
120
+
121
+ if (localFilePath) {
122
+ // Keep the raw import string as the lookup key (importUrl) so that
123
+ // data-src in the bundled HTML matches what the runtime's loadFromDOM
124
+ // will look up before getUrl transforms the path.
94
125
  return {
95
- schemaImport: schemaImport,
96
- schemaEntry: importIsObject ? { ...importValue, url: schemaImport } : schemaImport,
97
- importUrl: toRuntimeUrl(schemaImport),
98
- localFilePath: path.resolve(cwd, localSchemaPath),
126
+ schemaImport: importUrlValue,
127
+ schemaEntry: importIsObject ? { ...importValue, url: importUrlValue } : importUrlValue,
128
+ importUrl: importUrlValue,
129
+ localFilePath,
99
130
  remoteUrl: null
100
131
  }
101
132
  }
102
133
 
134
+ // Remote: npm package or explicit HTTP URL
103
135
  const remoteUrl = isHttpUrl(importUrlValue)
104
136
  ? importUrlValue
105
137
  : `https://cdn.jsdelivr.net/npm/${importUrlValue}`
@@ -108,7 +140,7 @@ function resolveFetchImport (importValue, modelUrl, cwd) {
108
140
  schemaEntry: importIsObject ? { ...importValue, url: importUrlValue } : importUrlValue,
109
141
  importUrl: toRuntimeUrl(importUrlValue),
110
142
  localFilePath: null,
111
- remoteUrl: remoteUrl
143
+ remoteUrl
112
144
  }
113
145
  }
114
146
 
@@ -1053,6 +1085,7 @@ Documentation: https://jsee.org
1053
1085
 
1054
1086
  module.exports = gen
1055
1087
  module.exports.collectFetchBundleBlocks = collectFetchBundleBlocks
1088
+ module.exports.resolveLocalImportFile = resolveLocalImportFile
1056
1089
  module.exports.resolveFetchImport = resolveFetchImport
1057
1090
  module.exports.resolveRuntimeMode = resolveRuntimeMode
1058
1091
  module.exports.resolveOutputPath = resolveOutputPath
package/src/main.js CHANGED
@@ -152,6 +152,7 @@ export default class JSEE {
152
152
  this.__version__ = VERSION
153
153
  this.cancelled = false
154
154
  this._cancelWorkerRun = null
155
+ this._workers = []
155
156
 
156
157
  // Check if schema is provided
157
158
  if (typeof this.schema === 'undefined') {
@@ -171,7 +172,7 @@ export default class JSEE {
171
172
  }
172
173
  }
173
174
 
174
- this.init()
175
+ this._initPromise = this.init()
175
176
  }
176
177
 
177
178
  log (...args) {
@@ -194,6 +195,35 @@ export default class JSEE {
194
195
  return this.cancelled === true
195
196
  }
196
197
 
198
+ destroy () {
199
+ log('Destroying JSEE instance')
200
+ // Cancel any running computation
201
+ this.cancelCurrentRun()
202
+ // Terminate all workers
203
+ this._workers.forEach(w => {
204
+ try { w.terminate() } catch (e) { /* ignore */ }
205
+ })
206
+ this._workers = []
207
+ // Unmount Vue app
208
+ if (this.app && this.app.__vueApp) {
209
+ this.app.__vueApp.unmount()
210
+ }
211
+ // Clean up overlay
212
+ if (this.overlay) {
213
+ this.overlay.hide()
214
+ }
215
+ // Remove progress bar
216
+ const progress = document.querySelector('#progress')
217
+ if (progress) progress.remove()
218
+ // Null out references
219
+ this.app = null
220
+ this.data = null
221
+ this.pipeline = null
222
+ this.model = null
223
+ this.schema = null
224
+ this._cancelWorkerRun = null
225
+ }
226
+
197
227
  progress (i) {
198
228
  const progressState = utils.getProgressState(i)
199
229
  if (!progressState) {
@@ -413,8 +443,11 @@ export default class JSEE {
413
443
  imp = m.imports[i]
414
444
  }
415
445
  if (!m.type.includes('py')) {
416
- imp.url = utils.getUrl(imp.url)
417
- imp.code = utils.loadFromDOM(imp.url)
446
+ imp.code = utils.loadFromDOM(imp.url) // Try raw path first (matches --fetch data-src)
447
+ imp.url = utils.getUrl(imp.url) // Resolve to absolute URL for network/worker
448
+ if (!imp.code) {
449
+ imp.code = utils.loadFromDOM(imp.url) // Fallback: resolved URL (legacy compat)
450
+ }
418
451
  }
419
452
  }
420
453
  }
@@ -578,6 +611,7 @@ export default class JSEE {
578
611
  async initWorker (model) {
579
612
  // Init worker
580
613
  const worker = new Worker()
614
+ this._workers.push(worker)
581
615
 
582
616
  // Init worker with the model
583
617
  if (typeof model.code === 'function') {
package/src/utils.js CHANGED
@@ -775,7 +775,15 @@ function debounce (fn, ms) {
775
775
  function getName (code) {
776
776
  switch (typeof code) {
777
777
  case 'function':
778
- return code.name
778
+ if (!code.name) return undefined
779
+ // Arrow functions get an inferred .name from property assignment
780
+ // (e.g. { code: (a) => a } → code.name === "code") which is misleading.
781
+ // Only trust .name when toString() confirms a real named declaration.
782
+ const src = code.toString().trimStart()
783
+ if (src.startsWith('function') || src.startsWith('async function')) {
784
+ return code.name
785
+ }
786
+ return undefined
779
787
  case 'string':
780
788
  const words = code.split(' ')
781
789
  const functionIndex = words.findIndex((word) => word === 'function')
@@ -0,0 +1,18 @@
1
+ <html>
2
+ <div id="jsee-container">
3
+ <script src="/dist/jsee.js"></script>
4
+ <script>
5
+ new JSEE({
6
+ schema: {
7
+ model: {
8
+ worker: false,
9
+ code: ({ a, b }) => ({ sum: Number(a) + Number(b) })
10
+ },
11
+ inputs: [
12
+ { name: 'a', type: 'int', default: 100 },
13
+ { name: 'b', type: 'int', default: 42 }
14
+ ]
15
+ }
16
+ })
17
+ </script>
18
+ </html>
@@ -0,0 +1,18 @@
1
+ <html>
2
+ <div id="jsee-container">
3
+ <script src="/dist/jsee.js"></script>
4
+ <script>
5
+ new JSEE({
6
+ schema: {
7
+ model: {
8
+ worker: true,
9
+ code: ({ a, b }) => ({ sum: Number(a) + Number(b) })
10
+ },
11
+ inputs: [
12
+ { name: 'a', type: 'int', default: 100 },
13
+ { name: 'b', type: 'int', default: 42 }
14
+ ]
15
+ }
16
+ })
17
+ </script>
18
+ </html>
@@ -0,0 +1,18 @@
1
+ <html>
2
+ <div id="jsee-container">
3
+ <script src="/dist/jsee.runtime.js"></script>
4
+ <script>
5
+ new JSEE({
6
+ schema: {
7
+ model: {
8
+ worker: true,
9
+ code: ({ a, b }) => ({ sum: Number(a) + Number(b) })
10
+ },
11
+ inputs: [
12
+ { name: 'a', type: 'int', default: 100 },
13
+ { name: 'b', type: 'int', default: 42 }
14
+ ]
15
+ }
16
+ })
17
+ </script>
18
+ </html>
@@ -86,6 +86,33 @@ describe('Minimal examples', () => {
86
86
  })
87
87
  })
88
88
 
89
+ describe('Arrow function as model.code', () => {
90
+ test('Main thread', async () => {
91
+ await page.goto(urlHTML('arrow-main'))
92
+ await expect(page).toFill('#a', '8')
93
+ await expect(page).toFill('#b', '7')
94
+ await expect(page).toClick('button', { text: 'Run' })
95
+ await expect(page).toMatchTextContent('15')
96
+ })
97
+ test('Worker', async () => {
98
+ await page.goto(urlHTML('arrow-worker'))
99
+ await expect(page).toFill('#a', '8')
100
+ await expect(page).toFill('#b', '7')
101
+ await expect(page).toClick('button', { text: 'Run' })
102
+ await expect(page).toMatchTextContent('15')
103
+ })
104
+ })
105
+
106
+ describe('Runtime build (jsee.runtime.js)', () => {
107
+ test('Arrow function in worker', async () => {
108
+ await page.goto(urlHTML('runtime-arrow'))
109
+ await expect(page).toFill('#a', '8')
110
+ await expect(page).toFill('#b', '7')
111
+ await expect(page).toClick('button', { text: 'Run' })
112
+ await expect(page).toMatchTextContent('15')
113
+ })
114
+ })
115
+
89
116
  describe('Load code directly', () => {
90
117
  test('Window', async () => {
91
118
  await page.goto(urlHTML('code'))
@@ -2,7 +2,7 @@ const fs = require('fs')
2
2
  const os = require('os')
3
3
  const path = require('path')
4
4
  const gen = require('../../src/cli')
5
- const { collectFetchBundleBlocks, resolveFetchImport, resolveRuntimeMode } = gen
5
+ const { collectFetchBundleBlocks, resolveLocalImportFile, resolveFetchImport, resolveRuntimeMode } = gen
6
6
 
7
7
  describe('collectFetchBundleBlocks', () => {
8
8
  test('collects model, view and render blocks', () => {
@@ -32,19 +32,107 @@ describe('collectFetchBundleBlocks', () => {
32
32
  })
33
33
  })
34
34
 
35
+ describe('resolveLocalImportFile', () => {
36
+ let tmpDir
37
+
38
+ beforeEach(() => {
39
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsee-resolve-'))
40
+ // Create a project layout:
41
+ // dist/a.js
42
+ // css/x.css
43
+ // src/model.js
44
+ // src/helper.js
45
+ fs.mkdirSync(path.join(tmpDir, 'dist'))
46
+ fs.mkdirSync(path.join(tmpDir, 'css'))
47
+ fs.mkdirSync(path.join(tmpDir, 'src'))
48
+ fs.writeFileSync(path.join(tmpDir, 'dist', 'a.js'), '// a')
49
+ fs.writeFileSync(path.join(tmpDir, 'css', 'x.css'), '/* x */')
50
+ fs.writeFileSync(path.join(tmpDir, 'src', 'model.js'), '// model')
51
+ fs.writeFileSync(path.join(tmpDir, 'src', 'helper.js'), '// helper')
52
+ })
53
+
54
+ afterEach(() => {
55
+ fs.rmSync(tmpDir, { recursive: true, force: true })
56
+ })
57
+
58
+ test('resolves bare-relative JS path from cwd', () => {
59
+ const result = resolveLocalImportFile('dist/a.js', 'src/model.js', tmpDir)
60
+ expect(result).toBe(path.join(tmpDir, 'dist', 'a.js'))
61
+ })
62
+
63
+ test('resolves bare-relative CSS path from cwd', () => {
64
+ const result = resolveLocalImportFile('css/x.css', 'src/model.js', tmpDir)
65
+ expect(result).toBe(path.join(tmpDir, 'css', 'x.css'))
66
+ })
67
+
68
+ test('resolves explicit-relative path against model directory', () => {
69
+ const result = resolveLocalImportFile('./helper.js', 'src/model.js', tmpDir)
70
+ expect(result).toBe(path.join(tmpDir, 'src', 'helper.js'))
71
+ })
72
+
73
+ test('returns null for nonexistent file', () => {
74
+ const result = resolveLocalImportFile('dist/nope.js', 'src/model.js', tmpDir)
75
+ expect(result).toBeNull()
76
+ })
77
+
78
+ test('returns null for HTTP URLs', () => {
79
+ const result = resolveLocalImportFile('https://cdn.example.com/lib.js', 'src/model.js', tmpDir)
80
+ expect(result).toBeNull()
81
+ })
82
+
83
+ test('returns null for npm package names', () => {
84
+ const result = resolveLocalImportFile('lodash', 'src/model.js', tmpDir)
85
+ expect(result).toBeNull()
86
+ })
87
+ })
88
+
35
89
  describe('resolveFetchImport', () => {
36
- test('resolves local relative import paths against model location', () => {
37
- const cwd = '/tmp/jsee-workspace'
38
- const result = resolveFetchImport('./helpers/math.js', 'apps/qrcode/model.js', cwd)
90
+ let tmpDir
91
+
92
+ beforeEach(() => {
93
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsee-resolve-'))
94
+ fs.mkdirSync(path.join(tmpDir, 'dist'))
95
+ fs.mkdirSync(path.join(tmpDir, 'css'))
96
+ fs.mkdirSync(path.join(tmpDir, 'src'))
97
+ fs.writeFileSync(path.join(tmpDir, 'dist', 'core.js'), '// core')
98
+ fs.writeFileSync(path.join(tmpDir, 'css', 'app.css'), '/* app */')
99
+ fs.writeFileSync(path.join(tmpDir, 'src', 'model.js'), '// model')
100
+ fs.writeFileSync(path.join(tmpDir, 'src', 'helper.js'), '// helper')
101
+ })
39
102
 
40
- expect(result.schemaImport).toBe('apps/qrcode/helpers/math.js')
41
- expect(result.importUrl).toBe('https://cdn.jsdelivr.net/npm/apps/qrcode/helpers/math.js')
42
- expect(result.localFilePath).toBe(path.join(cwd, 'apps/qrcode/helpers/math.js'))
103
+ afterEach(() => {
104
+ fs.rmSync(tmpDir, { recursive: true, force: true })
105
+ })
106
+
107
+ test('resolves bare-relative local JS with raw importUrl', () => {
108
+ const result = resolveFetchImport('dist/core.js', 'src/model.js', tmpDir)
109
+
110
+ expect(result.schemaImport).toBe('dist/core.js')
111
+ expect(result.importUrl).toBe('dist/core.js')
112
+ expect(result.localFilePath).toBe(path.join(tmpDir, 'dist', 'core.js'))
113
+ expect(result.remoteUrl).toBeNull()
114
+ })
115
+
116
+ test('resolves bare-relative local CSS with raw importUrl', () => {
117
+ const result = resolveFetchImport('css/app.css', 'src/model.js', tmpDir)
118
+
119
+ expect(result.schemaImport).toBe('css/app.css')
120
+ expect(result.importUrl).toBe('css/app.css')
121
+ expect(result.localFilePath).toBe(path.join(tmpDir, 'css', 'app.css'))
122
+ expect(result.remoteUrl).toBeNull()
123
+ })
124
+
125
+ test('resolves explicit-relative local import against model dir', () => {
126
+ const result = resolveFetchImport('./helper.js', 'src/model.js', tmpDir)
127
+
128
+ expect(result.schemaImport).toBe('./helper.js')
129
+ expect(result.importUrl).toBe('./helper.js')
130
+ expect(result.localFilePath).toBe(path.join(tmpDir, 'src', 'helper.js'))
43
131
  expect(result.remoteUrl).toBeNull()
44
132
  })
45
133
 
46
134
  test('keeps package imports as remote URLs', () => {
47
- const result = resolveFetchImport('chart.js', 'apps/qrcode/model.js', '/tmp/jsee-workspace')
135
+ const result = resolveFetchImport('chart.js', 'src/model.js', tmpDir)
48
136
 
49
137
  expect(result.schemaImport).toBe('chart.js')
50
138
  expect(result.importUrl).toBe('https://cdn.jsdelivr.net/npm/chart.js')
@@ -52,32 +140,33 @@ describe('resolveFetchImport', () => {
52
140
  expect(result.remoteUrl).toBe('https://cdn.jsdelivr.net/npm/chart.js')
53
141
  })
54
142
 
143
+ test('keeps remote HTTP URLs unchanged', () => {
144
+ const result = resolveFetchImport('https://cdn.example.com/lib.css', 'model.js', tmpDir)
145
+ expect(result.remoteUrl).toBe('https://cdn.example.com/lib.css')
146
+ expect(result.localFilePath).toBeNull()
147
+ })
148
+
55
149
  test('supports object imports and preserves extra fields', () => {
56
150
  const result = resolveFetchImport(
57
- { url: './helpers/math.js', integrity: 'sha-123' },
58
- 'apps/qrcode/model.js',
59
- '/tmp/jsee-workspace'
151
+ { url: './helper.js', integrity: 'sha-123' },
152
+ 'src/model.js',
153
+ tmpDir
60
154
  )
61
155
 
62
156
  expect(result.schemaEntry).toEqual({
63
- url: 'apps/qrcode/helpers/math.js',
157
+ url: './helper.js',
64
158
  integrity: 'sha-123'
65
159
  })
66
- expect(result.importUrl).toBe('https://cdn.jsdelivr.net/npm/apps/qrcode/helpers/math.js')
67
- expect(result.localFilePath).toBe(path.join('/tmp/jsee-workspace', 'apps/qrcode/helpers/math.js'))
68
- expect(result.remoteUrl).toBeNull()
69
- })
70
- test('resolves local CSS import paths', () => {
71
- const result = resolveFetchImport('./styles/app.css', 'apps/demo/model.js', '/tmp')
72
- expect(result.schemaImport).toBe('apps/demo/styles/app.css')
73
- expect(result.localFilePath).toBe(path.join('/tmp', 'apps/demo/styles/app.css'))
160
+ expect(result.importUrl).toBe('./helper.js')
161
+ expect(result.localFilePath).toBe(path.join(tmpDir, 'src', 'helper.js'))
74
162
  expect(result.remoteUrl).toBeNull()
75
163
  })
76
164
 
77
- test('keeps remote CSS imports as remote URLs', () => {
78
- const result = resolveFetchImport('https://cdn.example.com/lib.css', 'model.js', '/tmp')
79
- expect(result.remoteUrl).toBe('https://cdn.example.com/lib.css')
165
+ test('nonexistent file falls through to remote/CDN', () => {
166
+ const result = resolveFetchImport('dist/nope.js', 'src/model.js', tmpDir)
167
+
80
168
  expect(result.localFilePath).toBeNull()
169
+ expect(result.remoteUrl).toBe('https://cdn.jsdelivr.net/npm/dist/nope.js')
81
170
  })
82
171
  })
83
172
 
@@ -464,6 +464,26 @@ describe('getName', () => {
464
464
  function myFunc () {}
465
465
  expect(getName(myFunc)).toBe('myFunc')
466
466
  })
467
+ test('arrow function reference returns undefined (not inferred property name)', () => {
468
+ const obj = { code: (a, b) => a + b }
469
+ expect(getName(obj.code)).toBeUndefined()
470
+ })
471
+ test('anonymous function reference returns undefined', () => {
472
+ const fn = function () {}
473
+ // When assigned to a variable, .name is inferred, but toString() doesn't
474
+ // start with 'function <name>' — it starts with 'function ()'
475
+ // For this case getName should still work because fn.name = 'fn'
476
+ // and fn.toString() starts with 'function'
477
+ expect(getName(fn)).toBe('fn')
478
+ })
479
+ test('async function reference', () => {
480
+ async function fetchData () {}
481
+ expect(getName(fetchData)).toBe('fetchData')
482
+ })
483
+ test('async arrow function returns undefined', () => {
484
+ const obj = { code: async (a) => a }
485
+ expect(getName(obj.code)).toBeUndefined()
486
+ })
467
487
  test('non-string non-function returns undefined', () => {
468
488
  expect(getName(42)).toBeUndefined()
469
489
  expect(getName(null)).toBeUndefined()