@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/README.md +30 -10
- package/dist/jsee.core.js +1 -1
- package/dist/jsee.full.js +1 -1
- package/dist/jsee.runtime.js +1 -1
- package/package.json +4 -1
- package/src/cli.js +123 -8
- package/src/main.js +12 -0
- package/templates/common-outputs.js +30 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jseeio/jsee",
|
|
3
|
-
"version": "0.8.
|
|
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, //
|
|
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
|
-
-
|
|
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
|
|
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
|
-
//
|
|
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
|
|
1198
|
-
|
|
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
|
|
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
|
-
|
|
264
|
-
let
|
|
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:
|
|
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:
|
|
301
|
+
let blob = new Blob([content], { type: file.mime })
|
|
276
302
|
saveAs(blob, filename)
|
|
277
303
|
}
|
|
278
304
|
},
|