@jseeio/jsee 0.8.1 → 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.1",
3
+ "version": "0.8.2",
4
4
  "description": "JavaScript Execution Environment",
5
5
  "type": "commonjs",
6
6
  "main": "src/cli.js",
@@ -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",
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) {
@@ -751,14 +859,14 @@ async function gen (pargv, returnHtml=false) {
751
859
  description: 'd',
752
860
  port: 'p',
753
861
  version: 'v',
754
- fetch: 'f',
862
+ fetch: ['f', 'bundle', 'b'],
755
863
  execute: 'e',
756
864
  cdn: 'c',
757
865
  runtime: 'r',
758
866
  }
759
867
  const argvDefault = {
760
868
  execute: 'auto', // execute the model code on the server (auto = server-side when serving local .js models)
761
- fetch: false, // fetch the JSEE runtime from the CDN or local server
869
+ fetch: false, // bundle runtime, local code and imports into generated output
762
870
  inputs: 'schema.json', // default input is schema.json in the current working directory
763
871
  port: 3000, // default port for the server
764
872
  version: 'latest', // default version of JSEE runtime to use
@@ -920,7 +1028,8 @@ Options:
920
1028
  -d, --description <file> Markdown description file to include
921
1029
  -p, --port <number> Dev server port (default: 3000)
922
1030
  -v, --version <version> JSEE runtime version (default: latest)
923
- -f, --fetch Fetch and bundle runtime + dependencies into output
1031
+ -b, --bundle Bundle runtime + dependencies into output
1032
+ -f, --fetch Alias for --bundle
924
1033
  -e, --execute Execute model server-side (auto-enabled when serving local .js models)
925
1034
  --client Force client-side execution (disable auto server-side)
926
1035
  -c, --cdn <url|bool> Rewrite model URLs for CDN deployment
@@ -942,7 +1051,7 @@ Examples:
942
1051
  jsee schema.json --a=100 --b=200 Pass named data inputs
943
1052
  jsee schema.json data.csv Pass a file path as input
944
1053
  jsee schema.json -o app.html Generate static HTML file
945
- 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
946
1055
  jsee -p 8080 Start dev server on port 8080
947
1056
  jsee report.pdf Serve a PDF file (auto-detected viewer)
948
1057
  jsee data/ Serve a folder (file browser with preview)
@@ -1083,7 +1192,7 @@ Documentation: https://jsee.org
1083
1192
  }
1084
1193
  log('Argv:', argv)
1085
1194
 
1086
- // Initially in argv.fetch branch
1195
+ // Bundle/offline generation
1087
1196
  // Check if schema has model, convert to array if needed
1088
1197
  if (!schema.model) {
1089
1198
  // console.error('No model found in schema')
@@ -1194,8 +1303,9 @@ Documentation: https://jsee.org
1194
1303
  // Generate jsee code
1195
1304
  let jseeHtml = ''
1196
1305
  let hiddenElementHtml = ''
1197
- const runtimeMode = resolveRuntimeMode(argv.runtime, argv.fetch, hasOutputs)
1198
- if (argv.fetch) {
1306
+ const shouldBundle = Boolean(argv.fetch)
1307
+ const runtimeMode = resolveRuntimeMode(argv.runtime, shouldBundle, hasOutputs)
1308
+ if (shouldBundle) {
1199
1309
  // Fetch jsee code from the CDN or local server
1200
1310
  const jseeCode = await loadRuntimeCode(argv.version, schema)
1201
1311
  jseeHtml = `<script>${jseeCode}</script>`
@@ -1209,7 +1319,10 @@ Documentation: https://jsee.org
1209
1319
  }
1210
1320
  if (m.url) {
1211
1321
  // Fetch model from the local file system (remote URLs not yet supported here)
1212
- 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)
1213
1326
  hiddenElementHtml += `<script type="text/plain" style="display: none;" data-src="${m.url}">${modelCode}</script>`
1214
1327
  }
1215
1328
  const imports = toArray(m.imports)
@@ -1501,3 +1614,5 @@ module.exports.resolveFetchImport = resolveFetchImport
1501
1614
  module.exports.resolveRuntimeMode = resolveRuntimeMode
1502
1615
  module.exports.resolveOutputPath = resolveOutputPath
1503
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
  },