@jseeio/jsee 0.8.0 → 0.8.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.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "JavaScript Execution Environment",
5
5
  "type": "commonjs",
6
6
  "main": "src/cli.js",
@@ -17,27 +17,27 @@
17
17
  "default": "./src/cli.js"
18
18
  },
19
19
  "./core": {
20
- "require": "./dist/jsee.core.js",
20
+ "require": "./src/browser-bundle-node.js",
21
21
  "default": "./dist/jsee.core.js"
22
22
  },
23
23
  "./full": {
24
- "require": "./dist/jsee.full.js",
24
+ "require": "./src/browser-bundle-node.js",
25
25
  "default": "./dist/jsee.full.js"
26
26
  },
27
27
  "./runtime": {
28
- "require": "./dist/jsee.runtime.js",
28
+ "require": "./src/browser-bundle-node.js",
29
29
  "default": "./dist/jsee.runtime.js"
30
30
  },
31
31
  "./dist/jsee.core.js": {
32
- "require": "./dist/jsee.core.js",
32
+ "require": "./src/browser-bundle-node.js",
33
33
  "default": "./dist/jsee.core.js"
34
34
  },
35
35
  "./dist/jsee.full.js": {
36
- "require": "./dist/jsee.full.js",
36
+ "require": "./src/browser-bundle-node.js",
37
37
  "default": "./dist/jsee.full.js"
38
38
  },
39
39
  "./dist/jsee.runtime.js": {
40
- "require": "./dist/jsee.runtime.js",
40
+ "require": "./src/browser-bundle-node.js",
41
41
  "default": "./dist/jsee.runtime.js"
42
42
  },
43
43
  "./package.json": "./package.json"
@@ -120,6 +120,9 @@
120
120
  "vue3-json-viewer": "^2.2.2",
121
121
  "vuex": "^4.0.2"
122
122
  },
123
+ "optionalDependencies": {
124
+ "esbuild": "^0.28.0"
125
+ },
123
126
  "devDependencies": {
124
127
  "@babel/core": "^7.21.4",
125
128
  "@babel/plugin-transform-runtime": "^7.4.4",
@@ -0,0 +1,9 @@
1
+ 'use strict'
2
+
3
+ function browserBundleOnly () {
4
+ throw new Error('@jseeio/jsee browser bundles require a browser DOM. Use require("@jseeio/jsee") for the Node CLI/API, or load dist/jsee.core.js / dist/jsee.full.js in a browser.')
5
+ }
6
+
7
+ browserBundleOnly.browserOnly = true
8
+
9
+ module.exports = browserBundleOnly
package/src/cli.js CHANGED
@@ -259,6 +259,114 @@ function resolveOutputPath (cwd, outputPath) {
259
259
  return path.join(cwd, outputPath)
260
260
  }
261
261
 
262
+ let optionalEsbuild
263
+ function getOptionalEsbuild () {
264
+ if (optionalEsbuild) return optionalEsbuild
265
+ try {
266
+ optionalEsbuild = require('esbuild')
267
+ return optionalEsbuild
268
+ } catch (error) {
269
+ throw new Error('Bundling model dependencies requires optional dependency esbuild. Run `npm install esbuild` or use schema imports/prebundled browser code.')
270
+ }
271
+ }
272
+
273
+ function isJsIdentifier (value) {
274
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value)
275
+ }
276
+
277
+ function toJsIdentifier (value, fallback='model') {
278
+ const raw = value ? String(value) : fallback
279
+ const clean = raw.replace(/[^A-Za-z0-9_$]/g, '_')
280
+ if (!clean) return fallback
281
+ return /^[A-Za-z_$]/.test(clean) ? clean : '_' + clean
282
+ }
283
+
284
+ function getModelExportName (model) {
285
+ if (model.name) return model.name
286
+ if (model.url) return path.basename(model.url).replace(/\.[^.]+$/, '')
287
+ return 'model'
288
+ }
289
+
290
+ function hasCommonJsExport (code) {
291
+ return /\bmodule\s*\.\s*exports\b|\bexports\s*\.\s*[A-Za-z_$]/.test(code)
292
+ }
293
+
294
+ function hasEsModuleExport (code) {
295
+ return /^\s*export\s+(?:default|function|class|const|let|var|\{)/m.test(code)
296
+ }
297
+
298
+ function hasStaticImport (code) {
299
+ return /^\s*import\s+(?!\()/m.test(code)
300
+ }
301
+
302
+ function hasRequireCall (code) {
303
+ return /\brequire\s*\(/.test(code)
304
+ }
305
+
306
+ function shouldBundleModelCode (code) {
307
+ return hasRequireCall(code) || hasStaticImport(code) || hasCommonJsExport(code) || hasEsModuleExport(code)
308
+ }
309
+
310
+ function buildBundledModelExposeCode (bundleGlobalName, modelName) {
311
+ return `
312
+ ;(function () {
313
+ var mod = ${bundleGlobalName}
314
+ var target = mod
315
+ if (mod && (typeof mod === 'object')) {
316
+ if (typeof mod[${JSON.stringify(modelName)}] !== 'undefined') {
317
+ target = mod[${JSON.stringify(modelName)}]
318
+ } else if (typeof mod.default !== 'undefined') {
319
+ target = mod.default
320
+ }
321
+ }
322
+ globalThis[${JSON.stringify(modelName)}] = target
323
+ })()
324
+ `
325
+ }
326
+
327
+ async function bundleModelCode (model, modelPath, code) {
328
+ if (!shouldBundleModelCode(code)) return code
329
+
330
+ const esbuild = getOptionalEsbuild()
331
+ const modelName = getModelExportName(model)
332
+ const bundleGlobalName = `__jsee_bundle_${toJsIdentifier(modelName)}`
333
+ const needsExportShim = modelName &&
334
+ !hasCommonJsExport(code) &&
335
+ !hasEsModuleExport(code) &&
336
+ (hasRequireCall(code) || hasStaticImport(code))
337
+
338
+ if (needsExportShim && !isJsIdentifier(modelName)) {
339
+ throw new Error(`Bundling ${model.url || modelPath} requires a valid JavaScript model name or an explicit module export.`)
340
+ }
341
+
342
+ const options = {
343
+ bundle: true,
344
+ write: false,
345
+ platform: 'browser',
346
+ format: 'iife',
347
+ globalName: bundleGlobalName,
348
+ logLevel: 'silent'
349
+ }
350
+
351
+ if (needsExportShim) {
352
+ options.stdin = {
353
+ contents: `${code}\nexport default ${modelName}\n`,
354
+ resolveDir: path.dirname(modelPath),
355
+ sourcefile: path.basename(modelPath),
356
+ loader: 'js'
357
+ }
358
+ } else {
359
+ options.entryPoints = [modelPath]
360
+ }
361
+
362
+ try {
363
+ const result = await esbuild.build(options)
364
+ return result.outputFiles[0].text + buildBundledModelExposeCode(bundleGlobalName, modelName)
365
+ } catch (error) {
366
+ throw new Error(`Failed to bundle ${model.url || modelPath}: ${error.message}`)
367
+ }
368
+ }
369
+
262
370
  const FULL_BUNDLE_TYPES = ['chart', '3d', 'map']
263
371
 
264
372
  function needsFullBundle (schema) {
@@ -739,7 +847,9 @@ function template(schema, blocks) {
739
847
 
740
848
  async function gen (pargv, returnHtml=false) {
741
849
  // Determine if JSEE CLI is imported or run directly
742
- const imported = path.dirname(__dirname) !== path.dirname(require.main.path)
850
+ const mainPath = require.main && require.main.path ? require.main.path : process.cwd()
851
+ const mainFilename = require.main && require.main.filename ? require.main.filename : ''
852
+ const imported = path.dirname(__dirname) !== path.dirname(mainPath)
743
853
 
744
854
  // First pass over CLI arguments
745
855
  // JSEE-level args
@@ -749,14 +859,14 @@ async function gen (pargv, returnHtml=false) {
749
859
  description: 'd',
750
860
  port: 'p',
751
861
  version: 'v',
752
- fetch: 'f',
862
+ fetch: ['f', 'bundle', 'b'],
753
863
  execute: 'e',
754
864
  cdn: 'c',
755
865
  runtime: 'r',
756
866
  }
757
867
  const argvDefault = {
758
868
  execute: 'auto', // execute the model code on the server (auto = server-side when serving local .js models)
759
- fetch: false, // fetch the JSEE runtime from the CDN or local server
869
+ fetch: false, // bundle runtime, local code and imports into generated output
760
870
  inputs: 'schema.json', // default input is schema.json in the current working directory
761
871
  port: 3000, // default port for the server
762
872
  version: 'latest', // default version of JSEE runtime to use
@@ -798,7 +908,7 @@ async function gen (pargv, returnHtml=false) {
798
908
  new JSEE({
799
909
  container: document.getElementById('jsee-container'),
800
910
  schema: {
801
- model: { code: function chat(message, history) { return { chat: 'You said: ' + message } }, worker: false },
911
+ model: { code: function chat(inputs) { return { chat: 'You said: ' + inputs.message } }, worker: false },
802
912
  inputs: [{ name: 'message', type: 'string', enter: true }],
803
913
  outputs: [{ name: 'chat', type: 'chat' }]
804
914
  }
@@ -821,7 +931,7 @@ async function gen (pargv, returnHtml=false) {
821
931
  new JSEE({
822
932
  container: document.getElementById('jsee-container'),
823
933
  schema: {
824
- model: { code: function greet(name, count) { return { greeting: (name + '\\n').repeat(count) } }, worker: false },
934
+ model: { code: function greet(inputs) { return { greeting: (inputs.name + '\\n').repeat(inputs.count) } }, worker: false },
825
935
  inputs: [
826
936
  { name: 'name', type: 'string', default: 'World' },
827
937
  { name: 'count', type: 'slider', min: 1, max: 10, default: 3 }
@@ -846,8 +956,8 @@ async function gen (pargv, returnHtml=false) {
846
956
  inputs: [{ name: 'message', type: 'string', enter: true }],
847
957
  outputs: [{ name: 'chat', type: 'chat' }]
848
958
  }, null, 2) + '\n'
849
- files['model.js'] = `function chat(message, history) {
850
- return { chat: 'You said: ' + message }
959
+ files['model.js'] = `function chat(inputs) {
960
+ return { chat: 'You said: ' + inputs.message }
851
961
  }
852
962
  `
853
963
  files['README.md'] = `# Chat
@@ -870,8 +980,8 @@ npx @jseeio/jsee schema.json
870
980
  ],
871
981
  outputs: [{ name: 'greeting', type: 'code' }]
872
982
  }, null, 2) + '\n'
873
- files['model.js'] = `function greet(name, count) {
874
- return { greeting: (name + '\\n').repeat(count) }
983
+ files['model.js'] = `function greet(inputs) {
984
+ return { greeting: (inputs.name + '\\n').repeat(inputs.count) }
875
985
  }
876
986
  `
877
987
  files['README.md'] = `# My App
@@ -918,7 +1028,8 @@ Options:
918
1028
  -d, --description <file> Markdown description file to include
919
1029
  -p, --port <number> Dev server port (default: 3000)
920
1030
  -v, --version <version> JSEE runtime version (default: latest)
921
- -f, --fetch Fetch and bundle runtime + dependencies into output
1031
+ -b, --bundle Bundle runtime + dependencies into output
1032
+ -f, --fetch Alias for --bundle
922
1033
  -e, --execute Execute model server-side (auto-enabled when serving local .js models)
923
1034
  --client Force client-side execution (disable auto server-side)
924
1035
  -c, --cdn <url|bool> Rewrite model URLs for CDN deployment
@@ -940,7 +1051,7 @@ Examples:
940
1051
  jsee schema.json --a=100 --b=200 Pass named data inputs
941
1052
  jsee schema.json data.csv Pass a file path as input
942
1053
  jsee schema.json -o app.html Generate static HTML file
943
- jsee schema.json -o app.html -f Generate self-contained HTML with bundled runtime
1054
+ jsee schema.json -o app.html --bundle Generate self-contained HTML with bundled runtime
944
1055
  jsee -p 8080 Start dev server on port 8080
945
1056
  jsee report.pdf Serve a PDF file (auto-detected viewer)
946
1057
  jsee data/ Serve a folder (file browser with preview)
@@ -971,8 +1082,8 @@ Documentation: https://jsee.org
971
1082
  log('Current working directory:', process.cwd())
972
1083
  log('Script location:', __dirname)
973
1084
  log('Script file:', __filename)
974
- log('Require location:', require.main.path)
975
- log('Require file:', require.main.filename)
1085
+ log('Require location:', mainPath)
1086
+ log('Require file:', mainFilename)
976
1087
 
977
1088
  let cwd = process.cwd()
978
1089
  let inputs = argv.inputs
@@ -1081,7 +1192,7 @@ Documentation: https://jsee.org
1081
1192
  }
1082
1193
  log('Argv:', argv)
1083
1194
 
1084
- // Initially in argv.fetch branch
1195
+ // Bundle/offline generation
1085
1196
  // Check if schema has model, convert to array if needed
1086
1197
  if (!schema.model) {
1087
1198
  // console.error('No model found in schema')
@@ -1192,8 +1303,9 @@ Documentation: https://jsee.org
1192
1303
  // Generate jsee code
1193
1304
  let jseeHtml = ''
1194
1305
  let hiddenElementHtml = ''
1195
- const runtimeMode = resolveRuntimeMode(argv.runtime, argv.fetch, hasOutputs)
1196
- if (argv.fetch) {
1306
+ const shouldBundle = Boolean(argv.fetch)
1307
+ const runtimeMode = resolveRuntimeMode(argv.runtime, shouldBundle, hasOutputs)
1308
+ if (shouldBundle) {
1197
1309
  // Fetch jsee code from the CDN or local server
1198
1310
  const jseeCode = await loadRuntimeCode(argv.version, schema)
1199
1311
  jseeHtml = `<script>${jseeCode}</script>`
@@ -1207,7 +1319,10 @@ Documentation: https://jsee.org
1207
1319
  }
1208
1320
  if (m.url) {
1209
1321
  // Fetch model from the local file system (remote URLs not yet supported here)
1210
- const modelCode = fs.readFileSync(path.join(cwd, m.url), 'utf8')
1322
+ const modelPath = path.join(cwd, m.url)
1323
+ if (!m.name) m.name = getModelExportName(m)
1324
+ const modelSource = fs.readFileSync(modelPath, 'utf8')
1325
+ const modelCode = await bundleModelCode(m, modelPath, modelSource)
1211
1326
  hiddenElementHtml += `<script type="text/plain" style="display: none;" data-src="${m.url}">${modelCode}</script>`
1212
1327
  }
1213
1328
  const imports = toArray(m.imports)
@@ -1499,3 +1614,5 @@ module.exports.resolveFetchImport = resolveFetchImport
1499
1614
  module.exports.resolveRuntimeMode = resolveRuntimeMode
1500
1615
  module.exports.resolveOutputPath = resolveOutputPath
1501
1616
  module.exports.needsFullBundle = needsFullBundle
1617
+ module.exports.shouldBundleModelCode = shouldBundleModelCode
1618
+ module.exports.bundleModelCode = bundleModelCode
package/src/main.js CHANGED
@@ -1010,6 +1010,18 @@ export default class JSEE {
1010
1010
  || (output.alias && res[output.alias])
1011
1011
  if (typeof r !== 'undefined') {
1012
1012
  log(`Updating output: ${output.name} with data: ${typeof r}`)
1013
+ if (output.type === 'file' && r && typeof r === 'object' && !Array.isArray(r)) {
1014
+ if (typeof r.filename === 'string') output.filename = r.filename
1015
+ if (typeof r.name === 'string' && !output.filename) output.filename = r.name
1016
+ if (typeof r.mime === 'string') output.mime = r.mime
1017
+ if (typeof r.contentType === 'string') output.mime = r.contentType
1018
+ if (Object.prototype.hasOwnProperty.call(r, 'content')) output.value = r.content
1019
+ else if (Object.prototype.hasOwnProperty.call(r, 'value')) output.value = r.value
1020
+ else if (Object.prototype.hasOwnProperty.call(r, 'data')) output.value = r.data
1021
+ else if (Object.prototype.hasOwnProperty.call(r, 'url')) output.value = r.url
1022
+ else output.value = r
1023
+ return
1024
+ }
1013
1025
  // Convert large base64 image data URLs to blob URLs for efficiency
1014
1026
  if (output.type === 'image' && typeof r === 'string'
1015
1027
  && r.startsWith('data:') && r.length > 50000) {
@@ -19,6 +19,27 @@ function stringify (v) {
19
19
  : JSON.stringify(v)
20
20
  }
21
21
 
22
+ function getFileDescriptor (output) {
23
+ const value = output.value
24
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
25
+ return {
26
+ filename: output.filename || output.name || 'output',
27
+ content: value,
28
+ mime: output.mime || output.contentType || 'application/octet-stream'
29
+ }
30
+ }
31
+ const has = (key) => Object.prototype.hasOwnProperty.call(value, key)
32
+ return {
33
+ filename: value.filename || value.name || output.filename || output.name || 'output',
34
+ content: has('content') ? value.content
35
+ : has('value') ? value.value
36
+ : has('data') ? value.data
37
+ : has('url') ? value.url
38
+ : value,
39
+ mime: value.mime || value.contentType || output.mime || output.contentType || 'application/octet-stream'
40
+ }
41
+ }
42
+
22
43
  const component = {
23
44
  props: ['output'],
24
45
  emits: ['notification'],
@@ -260,19 +281,24 @@ const component = {
260
281
  }
261
282
  },
262
283
  downloadFile () {
263
- let filename = this.output.filename || this.output.name || 'output'
264
- let value = this.output.value
284
+ const file = getFileDescriptor(this.output)
285
+ let filename = file.filename
286
+ let value = file.content
265
287
  if (typeof value === 'string' && value.startsWith('data:')) {
266
288
  fetch(value)
267
289
  .then(r => r.blob())
268
290
  .then(blob => saveAs(blob, filename))
269
291
  .catch(() => {
270
- let blob = new Blob([value], { type: 'application/octet-stream' })
292
+ let blob = new Blob([value], { type: file.mime })
271
293
  saveAs(blob, filename)
272
294
  })
273
295
  } else {
296
+ if (typeof Blob !== 'undefined' && value instanceof Blob) {
297
+ saveAs(value, filename)
298
+ return
299
+ }
274
300
  let content = typeof value === 'string' ? value : JSON.stringify(value)
275
- let blob = new Blob([content], { type: 'application/octet-stream' })
301
+ let blob = new Blob([content], { type: file.mime })
276
302
  saveAs(blob, filename)
277
303
  }
278
304
  },