=== jsee/package.json ===
{
"name": "@jseeio/jsee",
"version": "0.4.1",
"description": "JavaScript Execution Environment",
"main": "dist/jsee.js",
"unpkg": "dist/jsee.js",
"jsdelivr": "dist/jsee.js",
"private": false,
"scripts": {
"build-dev": "webpack --mode=development --progress --stats-children --env DEVELOPMENT",
"build": "webpack --mode=production --progress && webpack --mode=production --progress --env RUNTIME && npm test",
"watch-dev": "nodemon --watch . --ignore dist,.git --ext vue,js,css,html --exec 'npm run build-dev'",
"watch": "nodemon --watch . --ignore dist,.git --ext vue,js,css,html --exec 'npm run build-dev && npm run test:basic'",
"prepublishOnly": "npm run build",
"test": "npm run test:unit && npm run test:basic && npm run test:python",
"test:unit": "jest --config jest.unit.config.js",
"test:basic": "jest test/test-basic.test.js --detectOpenHandles",
"test:python": "jest test/test-python.test.js --detectOpenHandles",
"test-head": "HEADLESS=false npm test",
"lint": "eslint src/ test/"
},
"bin": {
"jsee": "./bin/jsee"
},
"author": "Anton Zemlyansky",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/jseeio/jsee"
},
"homepage": "https://jsee.org",
"bugs": {
"url": "https://github.com/jseeio/jsee/issues"
},
"dependencies": {
"@mdi/font": "^6.5.95",
"bulma": "^0.9.3",
"csv-parse": "^5.6.0",
"dom-to-image": "^2.6.0",
"express": "^4.21.2",
"file-saver": "^2.0.2",
"filtrex": "^2.2.3",
"jsdoc-to-markdown": "^8.0.1",
"katex": "^0.16.22",
"minimist": "^1.2.8",
"notyf": "^3.10.0",
"showdown": "^1.9.1",
"showdown-katex": "^0.8.0",
"vue": "^3.2.47",
"vue-file-picker": "^0.0.2",
"vue-style-loader": "^4.1.3",
"vue3-json-viewer": "^2.2.2",
"vuex": "^4.0.2"
},
"devDependencies": {
"@babel/core": "^7.21.4",
"@babel/plugin-transform-runtime": "^7.4.4",
"@babel/preset-env": "^7.16.5",
"babel-loader": "^9.1.2",
"css-loader": "^6.7.3",
"eslint": "^8.57.1",
"expect-puppeteer": "^11.0.0",
"http-server": "^14.1.1",
"jest": "^29.7.0",
"jest-puppeteer": "^11.0.0",
"nodemon": "^3.1.10",
"puppeteer": "^24.10.2",
"sass": "^1.83.4",
"sass-loader": "^13.2.2",
"source-map-loader": "^4.0.1",
"style-loader": "^3.3.2",
"terser-webpack-plugin": "^5.3.7",
"uglify-es": "^3.3.9",
"vue-loader": "^17.0.1",
"webpack": "^5.78.0",
"webpack-cli": "^5.0.1",
"worker-loader": "^3.0.8"
},
"resolutions": {
"ansi-regex": "5.0.1"
}
}
=== jsee/src/app.js ===
// import Vue from 'vue'
// import { createApp } from 'vue'
// import { createApp } from 'vue/dist/vue.esm-bundler'
// import { createApp, h } from 'vue/dist/vue.runtime.esm-bundler.js'
import { createApp, h } from 'vue' // <- resolved in webpack.config based on RUNTIME
import bulmaApp from '../templates/bulma-app.vue'
import bulmaInput from '../templates/bulma-input.vue'
import bulmaOutput from '../templates/bulma-output.vue'
const components = {
'bulma': {
'app': bulmaApp,
'input': bulmaInput,
'output': bulmaOutput
}
}
const filtrex = require('filtrex')
const JsonViewer = require('vue3-json-viewer').default
const { sanitizeName, debounce } = require('./utils.js')
function setInputValue (input, value) {
if (input.type === 'file') {
// For file inputs, we need to set the url
input.url = value
input.file = null // Reset file object
} else {
// For other inputs, we can set the value directly
input.value = value
}
}
function resetInputs (inputs, example) {
inputs.forEach((input, index) => {
const inputName = input.name ? sanitizeName(input.name) : `input_${index}`
if (example && input.name && example[input.name]) {
// Object (unsanitized)
setInputValue(input, example[input.name])
} else if (example && inputName && example[inputName]) {
// Object (sanitized)
setInputValue(input, example[inputName])
} else if (example && Array.isArray(example) && typeof example[index] !== 'undefined') {
// Array
setInputValue(input, example[index])
} else if (input.default) {
// Default value
setInputValue(input, input.default)
} else {
switch (input.type) {
case 'int':
case 'float':
case 'number':
input.value = 0
break
case 'string':
case 'text':
input.value = ''
break
case 'color':
input.value = '#000000'
break
case 'categorical':
case 'select':
input.value = input.options ? input.options[0] : ''
break
case 'bool':
case 'checkbox':
input.value = false
break
case 'file':
input.file = null
input.value = ''
break
case 'group':
resetInputs(input.elements)
break
default:
input.value = ''
}
}
})
}
function createVueApp (env, mountedCallback, logMain) {
function log () {
logMain('[Vue]', ...arguments)
}
// Vue's data is based on schema
const dataInit = env.schema
// Reset input values to default ones
resetInputs(dataInit.inputs)
if (!('outputs' in dataInit)) {
dataInit.outputs = []
}
// Flag that shows if data was changed from initial conditions
dataInit.dataChanged = false
// Flag for autorun feedback
dataInit.clickRun = false
function len(s) {
return s.length;
}
let filtrexOptions = {
extraFunctions: { len }
}
// Prepare functions that determine if inputs should be displayed
const displayFunctions = dataInit.inputs.map(input => {
if (input.display && input.display.length) {
const f = filtrex.compileExpression(input.display.replace(/\'/g, '"'), filtrexOptions)
return function DisplayConditionally (data) {
const inputObj = {}
data.inputs.filter(input => input.name).forEach(input => {
// Sanitize input name. This will allow to operate with human-readable names
// and use them as object keys. For example, 'Input 1' will be converted to 'input_1'
const inputNameSanitized = sanitizeName(input.name)
inputObj[input.name] = input.value
inputObj[inputNameSanitized] = input.value
})
return f(inputObj)
}
} else {
return function DisplayAlways () {
return true
}
}
})
// Determine a container for Vue app
const container = env.container
? (typeof env.container === 'string')
? document.querySelector(env.container)
: env.container
: document.body
// Determine a template and GUI framework
const framework = (env.schema.design && typeof env.schema.design.framework !== 'undefined')
? env.schema.design.framework
: 'bulma'
let template
let render
if (
env.schema.design
&& env.schema.design.template
&& (
typeof env.schema.design.template === 'string'
|| env.schema.design.template === false
)
) {
template = env.schema.design.template
render = null
} else {
template = null //' '
render = () => {
return h(components[framework].app)
}
}
log('Initializing Vue app...')
const app = createApp({
template,
render,
data () {
return dataInit
},
watch: {
inputs: {
deep: true,
immediate: false,
// schema.reactive enables global re-run on any input change (debounced).
// Per-input reactivity uses the 'inchange' event from inputs with reactive: true.
handler: debounce(function (v) {
this.dataChanged = true
if (env.schema.reactive) {
this.run('reactive')
}
}, 300)
}
},
mounted () {
mountedCallback(container)
},
methods: {
display (index) {
const res = index < displayFunctions.length
? displayFunctions[index](this.$data)
: true
return res
},
reset (example) {
// Reset input values to default ones
// If example is provided, use it as a new default
resetInputs(this.inputs, example)
this.$nextTick(() => {
this.dataChanged = false
})
},
run (caller) {
this.clickRun = true
// Catch to prevent unhandled rejection from button/autorun clicks
env.run(caller).catch(err => console.error('Run error:', err))
setTimeout(() => {
this.clickRun = false
}, 150)
},
notify (msg) {
env.notify(msg)
}
}
})
if (framework !== false) {
app.component('vue-app', components[framework].app)
app.component('vue-input', components[framework].input)
app.component('vue-output', components[framework].output)
}
// Json viewer
app.use(JsonViewer)
// Load Vue framework if present
if (framework in window) {
app.use(window[framework])
}
return app.mount(container) // After app.mount() it's not the same app
}
export { createVueApp }
=== jsee/src/cli.js ===
const fs = require('fs')
const path = require('path')
const os = require('os')
const crypto = require('crypto')
const minimist = require('minimist')
const jsdoc2md = require('jsdoc-to-markdown')
const showdown = require('showdown')
const showdownKatex = require('showdown-katex')
const converter = new showdown.Converter({
extensions: [
showdownKatex({
throwOnError: true,
displayMode: true,
errorColor: '#1500ff',
output: 'mathml'
}),
],
tables: true
})
showdown.setFlavor('github')
const { getModelFuncJS, sanitizeName } = require('./utils.js')
// left padding of multiple lines
function pad (str, len, start=0) {
return str.split('\n').map((s, i) => i >= start ? ' '.repeat(len) + s : s).join('\n')
}
function depad (str, len) {
return str.split('\n').map(s => s.slice(len)).join('\n')
}
function toArray (value) {
if (!value) return []
return Array.isArray(value) ? value : [value]
}
function collectFetchBundleBlocks (schema) {
return []
.concat(toArray(schema.model))
.concat(toArray(schema.view))
.concat(toArray(schema.render))
.filter(Boolean)
}
function isHttpUrl (value) {
return /^https?:\/\//i.test(value)
}
function toRuntimeUrl (value) {
try {
return (new URL(value)).href
} catch (error) {
return (new URL(value, 'https://cdn.jsdelivr.net/npm/')).href
}
}
function isLocalJsImport (value) {
if (typeof value !== 'string') return false
const lower = value.toLowerCase()
if (!lower.endsWith('.js') && !lower.includes('.js?')) return false
if (isHttpUrl(value)) return false
return value.startsWith('./') || value.startsWith('../') || value.startsWith('/') || value.startsWith('file://')
}
function isLocalCssImport (value) {
if (typeof value !== 'string') return false
const lower = value.toLowerCase()
if (!lower.endsWith('.css') && !lower.includes('.css?')) return false
if (isHttpUrl(value)) return false
return value.startsWith('./') || value.startsWith('../') || value.startsWith('/') || value.startsWith('file://')
}
function getImportUrlValue (importValue) {
if (typeof importValue === 'string') return importValue
if (importValue && typeof importValue === 'object' && typeof importValue.url === 'string') {
return importValue.url
}
return null
}
// Resolve an import path to a local file by checking candidate locations on disk.
// Returns the absolute file path if found, null otherwise.
// This replaces heuristic prefix detection (isLocalJsImport/isLocalCssImport) with
// a definitive filesystem check — no heuristic can reliably distinguish bare-relative
// local paths like "dist/core.js" from npm package subpaths like "chart.js/dist/chart.min.js".
function resolveLocalImportFile (importUrlValue, modelUrl, cwd) {
if (!importUrlValue || typeof importUrlValue !== 'string') return null
if (isHttpUrl(importUrlValue)) return null
const modelDir = modelUrl && !isHttpUrl(modelUrl) ? path.dirname(modelUrl) : '.'
const candidates = []
if (importUrlValue.startsWith('./') || importUrlValue.startsWith('../')) {
// Explicit relative: prefer model-relative (colocated helpers), fallback to cwd
candidates.push(path.resolve(cwd, path.join(modelDir, importUrlValue)))
candidates.push(path.resolve(cwd, importUrlValue))
} else if (importUrlValue.startsWith('/')) {
// Absolute from project root
candidates.push(path.resolve(cwd, importUrlValue.slice(1)))
} else {
// Bare relative (dist/core.js): prefer cwd (schema-relative), fallback to model-relative
candidates.push(path.resolve(cwd, importUrlValue))
candidates.push(path.resolve(cwd, path.join(modelDir, importUrlValue)))
}
for (const fp of candidates) {
if (fs.existsSync(fp) && fs.statSync(fp).isFile()) return fp
}
return null
}
function resolveFetchImport (importValue, modelUrl, cwd) {
const importUrlValue = getImportUrlValue(importValue)
if (!importUrlValue) return null
const importIsObject = importValue && typeof importValue === 'object'
const localFilePath = resolveLocalImportFile(importUrlValue, modelUrl, cwd)
if (localFilePath) {
// Keep the raw import string as the lookup key (importUrl) so that
// data-src in the bundled HTML matches what the runtime's loadFromDOM
// will look up before getUrl transforms the path.
return {
schemaImport: importUrlValue,
schemaEntry: importIsObject ? { ...importValue, url: importUrlValue } : importUrlValue,
importUrl: importUrlValue,
localFilePath,
remoteUrl: null
}
}
// Remote: npm package or explicit HTTP URL
const remoteUrl = isHttpUrl(importUrlValue)
? importUrlValue
: `https://cdn.jsdelivr.net/npm/${importUrlValue}`
return {
schemaImport: importUrlValue,
schemaEntry: importIsObject ? { ...importValue, url: importUrlValue } : importUrlValue,
importUrl: toRuntimeUrl(importUrlValue),
localFilePath: null,
remoteUrl
}
}
function resolveRuntimeMode (runtime, fetchEnabled, outputs) {
const requestedRuntime = runtime || 'auto'
const availableModes = ['auto', 'local', 'cdn', 'inline']
if (!availableModes.includes(requestedRuntime)) {
return requestedRuntime // custom URL/path passthrough
}
if (requestedRuntime !== 'auto') {
return requestedRuntime
}
if (fetchEnabled) {
return 'inline'
}
return outputs ? 'cdn' : 'local'
}
function resolveOutputPath (cwd, outputPath) {
if (path.isAbsolute(outputPath)) {
return outputPath
}
return path.join(cwd, outputPath)
}
async function loadRuntimeCode (version) {
if (version === 'dev') {
return fs.readFileSync(path.join(__dirname, '..', 'dist', 'jsee.js'), 'utf8')
}
if (version === 'latest') {
return fs.readFileSync(path.join(__dirname, '..', 'dist', 'jsee.runtime.js'), 'utf8')
}
const response = await fetch(`https://cdn.jsdelivr.net/npm/@jseeio/jsee@${version}/dist/jsee.runtime.js`)
return response.text()
}
function getDataFromArgv (schema, argv, loadFiles=true) {
let data = {}
if (schema.inputs) {
schema.inputs.forEach(inp => {
const inputName = sanitizeName(inp.name)
console.log('Processing input:', inp.name, 'as', inputName)
if (inputName in argv) {
switch (inp.type) {
case 'file':
if (!loadFiles) {
// If we don't want to load files, just set the value to the file path
data[inp.name] = argv[inputName]
break
} else if (fs.existsSync(argv[inputName])) {
data[inp.name] = fs.readFileSync(argv[inputName], 'utf8')
} else {
console.error(`File not found: ${argv[inputName]}`)
process.exit(1)
}
break
case 'int':
data[inp.name] = parseInt(argv[inputName], 10)
break
case 'float':
data[inp.name] = parseFloat(argv[inputName])
break
case 'string':
default:
data[inp.name] = argv[inputName]
}
}
})
}
return data
}
function genSchema (jsdocData) {
let schema = {
model: [],
inputs: [],
outputs: [],
}
for (let d of jsdocData) {
const model = {
name: d.name ? d.name : d.meta.filename.split('.')[0],
description: d.description ? d.description : '',
type: d.kind,
container: 'args',
url: path.relative(process.cwd(), path.join(d.meta.path, d.meta.filename)),
worker: false
}
if (d.requires) {
model.imports = d.requires.map(r => r.replace('module:', ''))
}
if (d.params) {
// Check if all params have the same name before '.'
const names = new Set(d.params.map(p => p.name.split('.')[0]))
if ((d.params.length > 1) && (names.size === 1)) {
// Object
model.container = 'object'
d.params.slice(1).forEach(p => {
const inp = {
name: p.name.split('.')[1],
type: p.type.names[0],
description: p.description,
}
if (p.defaultvalue) {
inp.default = p.defaultvalue
}
schema.inputs.push(inp)
})
} else {
// Array
model.container = 'args'
d.params.forEach(p => {
const inp = {
name: p.name,
type: p.type.names[0],
description: p.description,
}
if (p.defaultvalue) {
inp.default = p.defaultvalue
}
schema.inputs.push(inp)
})
}
}
if (d.returns) {
d.returns.forEach(r => {
r.name = r.name ? r.name : r.description.split('-')[0].trim()
r.description = r.description.split('-').slice(1).join('-').trim()
})
const names = new Set(d.returns.map(r => r.name.split('.')[0]))
if ((d.returns.length > 1) && (names.size === 1)) {
// Object
d.returns.slice(1).forEach(p => {
const out = {
name: p.name.split('.')[1],
type: p.type.names[0],
description: p.description,
}
schema.outputs.push(out)
})
} else {
// Array
d.returns.forEach(p => {
const out = {
name: p.name,
type: p.type.names[0],
description: p.description,
}
schema.outputs.push(out)
})
}
}
if (d.customTags) {
d.customTags.forEach(t => {
if (t.tag === 'worker') {
model.worker = true
}
})
}
schema.model.push(model)
}
return schema
}
function genHtmlFromSchema(schema) {
let htmlDescription = '
';
// Process the model section
if (schema.model && schema.model.length > 0) {
schema.model.forEach(model => {
htmlDescription += `
${model.name} `
if (model.description) {
htmlDescription += `
${model.description}
`
}
})
}
// Process the inputs section
if (schema.inputs && schema.inputs.length > 0) {
htmlDescription += '
Inputs ';
schema.inputs.forEach(input => {
htmlDescription += `${input.name} (${input.type})`
if (input.description) {
htmlDescription += ` - ${input.description}`
}
})
htmlDescription += ' ';
}
// Process the outputs section
if (schema.outputs && schema.outputs.length > 0) {
htmlDescription += '
Outputs ';
schema.outputs.forEach(output => {
htmlDescription += `${output.name} (${output.type})`
if (output.description) {
htmlDescription += ` - ${output.description}`
}
})
htmlDescription += ' ';
}
htmlDescription += '
';
return htmlDescription;
}
function genMarkdownFromSchema(schema) {
let markdownDescription = '';
// Process the model section
if (schema.model && schema.model.length > 0) {
schema.model.forEach(model => {
markdownDescription += `### **${model.name}**\n`;
if (model.description) {
markdownDescription += `${model.description}\n\n`;
}
});
}
// Process the inputs section
if (schema.inputs && schema.inputs.length > 0) {
markdownDescription += '#### Inputs\n';
schema.inputs.forEach(input => {
markdownDescription += `- **${input.name}** (${input.type})`;
if (input.description) {
markdownDescription += ` - ${input.description}`;
}
markdownDescription += '\n';
});
}
// Process the outputs section
if (schema.outputs && schema.outputs.length > 0) {
markdownDescription += '#### Outputs\n';
schema.outputs.forEach(output => {
markdownDescription += `- **${output.name}** (${output.type})`;
if (output.description) {
markdownDescription += ` - ${output.description}`;
}
markdownDescription += '\n';
});
}
return markdownDescription;
}
function template(schema, blocks) {
let title = 'jsee'
// let url = schema.page && schema.page.url ? schema.page.url : ''
let url = ('page' in schema && 'url' in schema.page) ? schema.page.url : ''
if (schema.title) {
title = schema.title
} else if (schema.page && schema.page.title) {
title = schema.page.title
} else if (schema.model) {
if (Array.isArray(schema.model)) {
title = schema.model[0].name
} else {
title = schema.model.name
}
}
return `
${title}
${blocks.gaHtml}
${blocks.hiddenElementHtml}
${blocks.descriptionHtml}
${blocks.jseeHtml}
`
}
async function gen (pargv, returnHtml=false) {
// Determine if JSEE CLI is imported or run directly
const imported = path.dirname(__dirname) !== path.dirname(require.main.path)
// First pass over CLI arguments
// JSEE-level args
const argvAlias = {
inputs: 'i',
outputs: 'o',
description: 'd',
port: 'p',
version: 'v',
fetch: 'f',
execute: 'e',
cdn: 'c',
runtime: 'r',
}
const argvDefault = {
execute: false, // execute the model code on the server
fetch: false, // fetch the JSEE runtime from the CDN or local server
inputs: 'schema.json', // default input is schema.json in the current working directory
port: 3000, // default port for the server
version: 'latest', // default version of JSEE runtime to use
verbose: false, // verbose mode
cdn: false,
runtime: 'auto'
}
let argv = minimist(pargv, {
alias: argvAlias,
default: argvDefault,
boolean: ['help', 'h'],
})
if (argv.help || argv.h) {
console.log(`
Usage: jsee [schema.json] [options]
Options:
-i, --inputs Input schema file (default: schema.json)
-o, --outputs Output HTML file path
-d, --description Markdown description file to include
-p, --port Dev server port (default: 3000)
-v, --version JSEE runtime version (default: latest)
-f, --fetch Fetch and bundle runtime + dependencies into output
-e, --execute Execute model server-side
-c, --cdn Rewrite model URLs for CDN deployment
-r, --runtime Runtime: auto|local|cdn|inline or a custom URL/path (default: auto)
--verbose Enable verbose logging
Examples:
jsee schema.json Start dev server with schema
jsee schema.json -o app.html Generate static HTML file
jsee schema.json -o app.html -f Generate self-contained HTML with bundled runtime
jsee -p 8080 Start dev server on port 8080
Documentation: https://jsee.org
`.trim())
return
}
// Set argv.inputs to the first non-option argument if it exists
if (!imported && argv._.length > 0 && !argv.inputs) {
argv.inputs = argv._[0]
}
function log (...args) {
if (argv.verbose) {
console.log('[JSEE CLI]', ...args)
}
}
log('Imported:', imported)
log('Current working directory:', process.cwd())
log('Script location:', __dirname)
log('Script file:', __filename)
log('Require location:', require.main.path)
log('Require file:', require.main.filename)
let cwd = process.cwd()
let inputs = argv.inputs
let outputs = argv.outputs
let description = argv.description
let schema
let schemaPath
let descriptionTxt = ''
let descriptionHtml = ''
let jsdocMarkdown = ''
let modelFuncs = {}
// Determine the inputs and outputs
// if inputs is a string with js file names, split it into an array
if (typeof inputs === 'string') {
if (inputs.includes('.js')) {
inputs = inputs.split(',')
}
}
// if outputs is a string with js file names, split it into an array
if (typeof outputs === 'string') {
outputs = outputs.split(',')
}
if (inputs.length === 0) {
console.error('No inputs provided')
process.exit(1)
} else if ((inputs.length === 1) && (inputs[0].includes('.json'))) {
// Input is json schema
// Curren working directory if not provided
// schema = require(path.join(cwd, inputs[0]))
// switch to fs.readFileSync to reload the schema if it changes
schemaPath = inputs[0].startsWith('/') ? inputs[0] : path.join(cwd, inputs[0])
schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'))
} else {
// Array of js files
// Generate schema
let jsdocData = jsdoc2md.getTemplateDataSync({ files: inputs.map(f => path.join(cwd, f)) })
schema = genSchema(jsdocData)
// jsdocMarkdown = jsdoc2md.renderSync({
// data: jsdocData,
// 'param-list-format': 'list',
// })
}
log('Schema path:', schemaPath)
// Second pass over CLI arguments
// Iterate over schema inputs and update argv aliases
if (schema.inputs) {
schema.inputs.forEach((inp, inp_index) => {
if (inp.name) {
const inputName = sanitizeName(inp.name)
if (inp.alias) {
argvAlias[inputName] = inp.alias
}
// Use positional arguments as schema inputs defaults if JSEE CLI is imported
if (imported && argv._.length > inp_index) {
log('Using positional argument for input:', inputName, argv._[inp_index])
argvDefault[inputName] = argv._[inp_index]
}
// We don't need to duplicate defaults here, as we handle them on the frontend
// else if (inp.default) {
// argvDefault[inputName] = inp.default
// }
}
})
}
// Update argv with the new aliases and defaults
argv = minimist(pargv, {
alias: argvAlias,
default: argvDefault,
})
// Now deactivate the inputs present in argv
// If you set parameter on the command line, it should not be editable in the GUI
// E.g. file selected
const dataFromArgvWithoutFileLoading = getDataFromArgv(schema, argv, false)
log('Data from argv without file loading:', dataFromArgvWithoutFileLoading)
if (schema.inputs) {
schema.inputs.forEach(inp => {
// Here data contains unsanitized input names
if (inp.name in dataFromArgvWithoutFileLoading) {
inp.default = dataFromArgvWithoutFileLoading[inp.name]
inp.disabled = true // Deactivate the input if it's present in argv
}
})
}
log('Argv:', argv)
// Initially in argv.fetch branch
// Check if schema has model, convert to array if needed
if (!schema.model) {
// console.error('No model found in schema')
// process.exit(1)
// It's still valid schema, can be only render function or vis of inputs/outputs
schema.model = []
}
if (!Array.isArray(schema.model)) {
schema.model = [schema.model]
}
// Server-side execution
// If execute is true, we will prepare the model functions to run on the server side
// Schema model will be updated with the server url and POST method
if (argv.execute) {
await Promise.all(schema.model.map(async m => {
log('Preparing a model to run on the server side:', m.name, m.url)
const target = require(path.join(schemaPath ? path.dirname(schemaPath) : cwd, m.url))
modelFuncs[m.name] = await getModelFuncJS(m, target, {log})
m.type = 'post'
m.url = `/${m.name}`
m.worker = false
}))
}
// Switch to CDN for model files
if (argv.cdn) {
let cdn = ''
console.log(argv)
if (typeof argv.cdn === 'string') {
cdn = argv.cdn
} else if (typeof argv.cdn === 'boolean') {
// Check package.json in cwd
const packageJsonPath = path.join(cwd, 'package.json')
if (fs.existsSync(packageJsonPath)) {
const target = require(packageJsonPath)
const packageName = target.name
cdn = `https://cdn.jsdelivr.net/npm/${packageName}@${target.version}/`
} else {
console.error(`No package.json found: ${packageJsonPath}`)
process.exit(1)
}
} else {
console.error('Invalid CDN argument. Use --cdn or --cdn true to use package.json version.')
process.exit(1)
}
log('Using CDN for model files:', cdn)
schema.model.forEach(m => {
if (m.url) {
// If url is relative, make it absolute
if (!m.url.startsWith('http')) {
m.url = path.join(cdn, m.url)
log(`Updated ${m.name} model URL to: ${m.url}`)
}
}
})
}
log('Schema:', schema)
// Generate description block
if (description) {
const descriptionMd = fs.readFileSync(path.join(cwd, description), 'utf8')
descriptionHtml = converter.makeHtml(descriptionMd)
if (descriptionMd.includes('---')) {
descriptionTxt = descriptionMd
.split('---')[0]
.replace(/\n/g, ' ')
.replace(/\s+/g, ' ')
.replace(/#/g, '')
.replace(/\*/g, '')
.trim()
}
}
descriptionHtml += genHtmlFromSchema(schema)
// Generate jsee code
let jseeHtml = ''
let hiddenElementHtml = ''
const hasOutputs = Array.isArray(outputs) ? outputs.length > 0 : Boolean(outputs)
const runtimeMode = resolveRuntimeMode(argv.runtime, argv.fetch, hasOutputs)
if (argv.fetch) {
// Fetch jsee code from the CDN or local server
const jseeCode = await loadRuntimeCode(argv.version)
jseeHtml = ``
// Fetch model files and store them in hidden elements
hiddenElementHtml += ''
const bundleBlocks = collectFetchBundleBlocks(schema)
for (let m of bundleBlocks) {
if (m.type === 'get' || m.type === 'post') {
continue
}
if (m.url) {
// Fetch model from the local file system (remote URLs not yet supported here)
const modelCode = fs.readFileSync(path.join(cwd, m.url), 'utf8')
hiddenElementHtml += ``
}
const imports = toArray(m.imports)
if (imports.length) {
m.imports = imports
for (let [index, i] of imports.entries()) {
if (typeof i !== 'string') {
continue
}
const importMeta = resolveFetchImport(i, m.url, cwd)
if (!importMeta) {
continue
}
m.imports[index] = importMeta.schemaEntry
let importCode
if (importMeta.localFilePath) {
importCode = fs.readFileSync(importMeta.localFilePath, 'utf8')
} else {
// Create cache directory if it doesn't exist
const cacheDir = path.join(os.homedir(), '.cache', 'jsee')
fs.mkdirSync(cacheDir, { recursive: true })
// Create a hash of the importUrl
const hash = crypto.createHash('sha256').update(importMeta.importUrl).digest('hex')
const cacheFilePath = path.join(cacheDir, `${hash}.js`)
let useCache = false
// Check if cache file exists and is less than 1 day old
if (fs.existsSync(cacheFilePath)) {
const stats = fs.statSync(cacheFilePath)
const mtime = new Date(stats.mtime)
const now = new Date()
const ageInDays = (now - mtime) / (1000 * 60 * 60 * 24)
if (ageInDays < 1) {
log('Using cached import:', importMeta.importUrl)
importCode = fs.readFileSync(cacheFilePath, 'utf8')
useCache = true
}
}
if (!useCache) {
const response = await fetch(importMeta.remoteUrl)
if (!response.ok) {
console.error(`Failed to fetch ${importMeta.remoteUrl}: ${response.statusText}`)
process.exit(1)
}
importCode = await response.text()
fs.writeFileSync(cacheFilePath, importCode, 'utf8')
log('Fetched and stored to cache:', importMeta.importUrl)
}
}
hiddenElementHtml += ``
}
}
}
hiddenElementHtml += '
'
} else {
if (runtimeMode === 'inline') {
const jseeCode = await loadRuntimeCode(argv.version)
jseeHtml = ``
} else if (runtimeMode === 'cdn') {
jseeHtml = ``
} else if (runtimeMode === 'local') {
jseeHtml = argv.version === 'dev'
? ``
: ``
} else {
// Custom path/URL passed via --runtime (e.g. ./node_modules/.../jsee.js)
jseeHtml = ``
}
}
let socialHtml = ''
let gaHtml = ''
let orgHtml = ''
if (schema.page) {
if (schema.page.ga) {
gaHtml = `
`
}
// Social media links
if (schema.page.social) {
// iterate over dict with k, v pairs
for (let [name, url] of Object.entries(schema.page.social)) {
switch (name) {
case 'twitter':
socialHtml += `Twitter `
break
case 'github':
socialHtml += `GitHub `
break
case 'facebook':
socialHtml += `Facebook `
break
case 'linkedin':
socialHtml += `LinkedIn `
break
case 'instagram':
socialHtml += `Instagram `
break
case 'youtube':
socialHtml += `YouTube `
break
default:
socialHtml += `${name} `
}
}
}
if (schema.page.org) {
orgHtml = `'
}
}
const html = template(schema, {
descriptionHtml: pad(descriptionHtml, 8, 1),
descriptionTxt: descriptionTxt,
gaHtml: pad(gaHtml, 2, 1),
jseeHtml: jseeHtml,
hiddenElementHtml: hiddenElementHtml,
socialHtml: pad(socialHtml, 2, 1),
orgHtml: pad(orgHtml, 2, 1),
})
if (returnHtml) {
// Return the html as a string
return html
} else if (outputs) {
// Store the html in the output file
for (let o of outputs) {
if (o === 'stdout') {
log(html)
} else if (o.includes('.html')) {
fs.writeFileSync(resolveOutputPath(cwd, o), html)
} else if (o.includes('.json')) {
fs.writeFileSync(resolveOutputPath(cwd, o), JSON.stringify(schema, null, 2))
} else if (o.includes('.md')) {
fs.writeFileSync(resolveOutputPath(cwd, o), genMarkdownFromSchema(schema))
} else {
console.error('Invalid output file:', o)
}
}
} else {
// Serve the html
const express = require('express')
const app = express()
app.use(express.json())
if (argv.execute) {
// Create post endpoint for executing the model
schema.model.forEach(m => {
app.post(m.url, (req, res) => {
log(`Executing model: ${m.name}`)
if (m.name in modelFuncs) {
const modelFunc = modelFuncs[m.name]
try {
const dataFromArgv = getDataFromArgv(schema, argv)
const dataFromGUI = req.body
const data = { ...dataFromGUI, ...dataFromArgv }
log('Data for model execution:', data)
const result = modelFunc(data)
res.json(result)
log(`Model ${m.name} executed successfully: `, result)
} catch (error) {
console.error('Error executing model:', error)
res.status(500).json({ error: error.message })
}
}
})
log('Model execution endpoints created:', m.url)
})
}
app.get('/', async (req, res) => {
log('Serving index.html')
res.send(await gen(pargv, true))
})
// app.get('/dist/jsee.runtime.js', (req, res) => {
// // __dirname points to this file location (it's jsee/src/cli.js, likely in node_modules)
// // so we need to go up one level to get to the dist folder with jsee.runtime.js
// const pathToJSEE = path.join(__dirname, '..', 'dist', 'jsee.runtime.js')
// log(`Serving jsee.runtime.js from: ${pathToJSEE}`)
// res.sendFile(pathToJSEE)
// })
app.use('/dist', express.static(path.join(__dirname, '..', 'dist'))) // Serve static files from the dist folder
// app.use(express.static(cwd))
// app.use(express.static(require.main.path)) // Serve static files from the main module path
// app.use(express.static(path.join(require.main.path, '..'))) // Serve static files from the parent directory of the main module path
app.use(express.static(schemaPath ? path.dirname(schemaPath) : cwd)) // Serve static files from the schema path or current working directory
app.listen(argv.port, () => {
console.log(`JSEE app is running: http://localhost:${argv.port}`)
})
}
}
module.exports = gen
module.exports.collectFetchBundleBlocks = collectFetchBundleBlocks
module.exports.resolveLocalImportFile = resolveLocalImportFile
module.exports.resolveFetchImport = resolveFetchImport
module.exports.resolveRuntimeMode = resolveRuntimeMode
module.exports.resolveOutputPath = resolveOutputPath
=== jsee/src/constants.js ===
// Default values and constants used across JSEE modules
const DEFAULT_CONTAINER = '#jsee-container'
const DEFAULT_WORKER_TIMEOUT = 30000
const DEFAULT_CHUNK_SIZE = 256 * 1024
const STREAM_HIGH_WATER = 4
module.exports = {
DEFAULT_CONTAINER,
DEFAULT_WORKER_TIMEOUT,
DEFAULT_CHUNK_SIZE,
STREAM_HIGH_WATER,
}
=== jsee/src/main.js ===
import { createVueApp } from './app'
import Worker from './worker.js'
const utils = require('./utils')
const isObject = utils.isObject
const { DEFAULT_CONTAINER, DEFAULT_WORKER_TIMEOUT } = require('./constants')
const { Notyf } = require('notyf')
const notyf = new Notyf({
types: [
{
type: 'success',
background: '#00d1b2',
},
{
type: 'error',
background: '#f14668',
duration: 2000,
dismissible: true
}
]
})
const Overlay = require('./overlay')
require('notyf/notyf.min.css')
const fetch = window['fetch']
const Blob = window['Blob']
let verbose = true
function log () {
if (verbose) {
console.log(`[JSEE v${VERSION}]`, ...arguments)
const logElement = document.querySelector('#log')
if (logElement) {
logElement.innerHTML += `\n${[...arguments].join(' ')}`
logElement.scrollTop = logElement.scrollHeight // auto scroll to bottom
if (logElement.innerHTML.length > 10000) {
logElement.innerHTML = logElement.innerHTML.slice(-10000)
}
}
}
}
// const Worker = window['Worker']
// Deep clone a simple object
function clone (obj) {
// return JSON.parse(JSON.stringify(obj))
return Object.assign({}, obj)
}
const getName = utils.getName
// Return input value
function getValue (input) {
if (input.type === 'group') {
const value = {}
input.elements.forEach(el => {
value[el.name] = getValue(el)
})
return value
} else {
return input.value
}
}
function getModelType (model) {
if (typeof model.code === 'string' && model.code.trim().length > 0) {
if (model.code.split(' ').map(v => v.trim()).includes('def')) {
return 'py'
}
return 'function'
}
if (model.url) {
return 'post'
}
return 'function'
}
// Nice trick to get a function parameters by Jack Allan
// From: https://stackoverflow.com/a/9924463/2998960
const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg
const ARGUMENT_NAMES = /([^\s,]+)/g
function getParamNames (func) {
const fnStr = func.toString().replace(STRIP_COMMENTS, '')
let result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(ARGUMENT_NAMES)
if (result === null)
result = []
return result
}
function getInputs (model) {
if (model.code) {
const params = getParamNames(model.code).filter(p => !['(', ')', '#', '{', '}'].some(c => p.includes(c)))
log('Trying to infer inputs from params:', params)
return params.map(p => ({
'name': p,
'type': 'string'
}))
}
return []
}
function collectStreamInputConfig (inputs, config={}) {
if (!Array.isArray(inputs)) {
return config
}
inputs.forEach(input => {
if (!isObject(input)) {
return
}
if (input.type === 'group') {
collectStreamInputConfig(input.elements, config)
return
}
if (input.type === 'file' && input.stream === true && input.name) {
config[input.name] = { stream: true }
}
})
return config
}
function getFunctionContainer (target) {
// Check if the number of parameters is > 1, then 'args'
}
export default class JSEE {
constructor (params, alt1, alt2) {
// Check if JSEE was initialized with args rather than with a params object
// So two ways to init JSEE:
// 1. new JSEE({schema: ..., container: ..., verbose: ...}) <- params object
// 2. new JSEE(schema, container, verbose) <- args
// This check converts args to params object (2 -> 1)
if (('model' in params) || (typeof params === 'string') || (typeof params === 'function') || !(typeof alt1 === 'undefined')) {
params = {
'schema': params,
'container': alt1,
'verbose': alt2
}
}
// Set global verbose flag
// This check sets verbose to true in all cases except when params.verbose is explicitly set to false
verbose = !(params.verbose === false)
this.container = params.container
this.schema = params.schema || params.config // Previous naming
this.utils = utils
this.__version__ = VERSION
this.cancelled = false
this._cancelWorkerRun = null
// Check if schema is provided
if (typeof this.schema === 'undefined') {
notyf.error('No schema provided')
throw new Error('No schema provided')
}
// Check if container is provided
if (typeof this.container === 'undefined') {
// Check if 'jsee-container' exists
if (document.querySelector(DEFAULT_CONTAINER)) {
this.container = DEFAULT_CONTAINER
log(`Using default container: ${this.container}`)
} else {
notyf.error('No container provided')
throw new Error('No container provided')
}
}
this.init()
}
log (...args) {
log(...args)
}
notify (txt) {
notyf.success(txt)
}
cancelCurrentRun () {
log('Stopping current run')
this.cancelled = true
if (typeof this._cancelWorkerRun === 'function') {
this._cancelWorkerRun()
}
}
isCancelled () {
return this.cancelled === true
}
progress (i) {
const progressState = utils.getProgressState(i)
if (!progressState) {
return
}
// Check if progress div is defined
let progress = document.querySelector('#progress')
if (!progress && progressState.mode === 'determinate' && progressState.value === 0) {
return
}
if (!progress) {
progress = document.createElement('div')
progress.setAttribute('id', 'progress')
progress.style = 'position: fixed; top: 0; left: 0; width: 0; height: 3px; background: #00d1b2; z-index: 1000;'
document.body.appendChild(progress)
}
let progressStyle = document.querySelector('#jsee-progress-style')
if (!progressStyle) {
progressStyle = document.createElement('style')
progressStyle.setAttribute('id', 'jsee-progress-style')
progressStyle.textContent = `
@keyframes jsee-progress-indeterminate {
0% { transform: translateX(-120%); }
100% { transform: translateX(360%); }
}
`
document.head.appendChild(progressStyle)
}
if (progressState.mode === 'indeterminate') {
progress.style.width = '30%'
progress.style.animation = 'jsee-progress-indeterminate 1.2s ease-in-out infinite'
} else {
progress.style.animation = 'none'
progress.style.transform = 'none'
progress.style.width = `${progressState.value}%`
}
}
async init () {
// At this point this.schema is defined but can be in different forms (e.g. string, object, function)
await this.initSchema() // Inits: this.schema (object)
await this.initModel() // Inits: this.model (array of objects)
await this.initInputs() // Inits: schema inputs based on url
this.streamInputConfig = collectStreamInputConfig(this.schema.inputs)
await this.initVue() // Inits: this.app, this.data
await this.initPipeline() // Inits: this.pipeline (function)
if (this.schema.autorun || this.schema.inputs.some(input => input.disabled && input.reactive)) {
// 1. If autorun is enabled in the schema, run the model immediately
// 2. Server-side inputs: If there are inputs with disabled and reactive flags
// (we assume that they are set by the server and trigger the model run)
log('[Init] First run of the model due to autorun or reactive inputs')
// Catch here to prevent unhandled rejection from init-time run
this.run('init').catch(err => log('Init run error:', err))
}
}
async initSchema () {
// Check if schema is a string (url to json)
if (typeof this.schema === 'string') {
// indexOf returns -1 (truthy) when not found, so use includes instead
this.schemaUrl = this.schema.includes('.json') ? this.schema : this.schema + '.json'
// Check if schema is present in the hidden DOM element
const schema = utils.loadFromDOM(this.schemaUrl)
if (schema) {
// Schema block found in the hidden element, use its content
this.schema = JSON.parse(schema);
log(`Loaded schema from the hidden DOM element for ${this.schemaUrl}:`, this.schema);
} else {
// Fetch schema from the URL
log('Fetching schema from:', this.schemaUrl)
this.schema = await fetch(this.schemaUrl)
this.schema = await this.schema.json()
log('Loaded schema from URL:', this.schema)
}
}
// Check if schema is a function (model)
if (typeof this.schema === 'function') {
this.schema = {
model: {
code: this.schema,
}
}
}
// At this point schema should be an object
if (!isObject(this.schema)) {
notyf.error('Schema is in a wrong format')
throw new Error(`Schema is in a wrong format: ${this.schema}`)
}
// Validate schema shape early so init stages fail fast on critical issues
const schemaValidation = utils.validateSchema(this.schema)
schemaValidation.warnings.forEach(warning => {
log('[Schema validation warning]', warning)
})
if (schemaValidation.errors.length) {
const firstError = schemaValidation.errors[0]
notyf.error(firstError)
throw new Error(`Schema validation failed: ${schemaValidation.errors.join('; ')}`)
}
}
async initModel () {
// Model is the main part of the schema that defines all computations
// At the end it should be an array of objects that define a sequence of tasks
this.model = []
// Check if there's a render or view defined in the schema
let view = this.schema.render || this.schema.view
if (isObject(view)) {
// If view is an object, convert it to an array
view = [view] // Convert to array if it's an object
}
if (Array.isArray(view)) {
view.forEach(v => {
v.worker = false // Render should not be in a worker
})
log('View is defined in the schema')
}
// Check if model is a function (model)
;[this.schema.model, view].forEach(m => {
// Function -> {code: Function}
if (typeof m === 'function') {
this.model.push({
code: m
})
} else if (Array.isArray(m)) {
// concatenate
this.model = this.model.concat(m)
} else if (isObject(m)) {
this.model.push(m)
}
})
// Check if model is empty
if (this.model.length === 0) {
notyf.error('Model is in a wrong format')
throw new Error(`Model is in a wrong format: ${this.schema.model}`)
}
// Put worker and imports inside model blocks
;['worker', 'imports'].forEach(key => {
if (typeof this.schema[key] !== 'undefined') {
this.model[0][key] = this.schema[key]
delete this.schema[key]
}
})
// Check if autorun is defined
if (typeof this.model[0]['autorun'] !== 'undefined') {
this.schema.autorun = this.model[0]['autorun']
delete this.model[0]['autorun']
}
// Async for-loop over this.model
for (const [i, m] of this.model.entries()) {
if (typeof m.worker === 'undefined') {
m.worker = i === 0 // Run first model in a web worker
}
// Load code if url is provided
if (m.url && (m.url.includes('.js') || m.url.includes('.py'))) {
// Try to get the code from a hidden DOM element first
const modelCode = utils.loadFromDOM(m.url)
if (modelCode) {
// Code block found in the hidden element, use its content
m.code = modelCode
log(`Loaded code from the hidden DOM element for ${m.url}`);
} else {
// Update model URL if needed
if (!m.url.includes('/') && this.schemaUrl && this.schemaUrl.includes('/')) {
m.url = window.location.protocol + '//' + window.location.host + this.schemaUrl.split('/').slice(0, -1).join('/') + '/' + m.url
log(`Changed the old model URL to ${m.url} (based on the schema URL)`)
}
log('Loaded code from:', m.url)
m.code = await fetch(m.url)
m.code = await m.code.text()
}
}
// Update model name if absent
if (typeof m.name === 'undefined'){
if ((m.url) && (m.url.includes('.js'))) {
m.name = m.url.split('/').pop().split('.')[0]
log('Use model name from url:', m.name)
} else if (m.code) {
m.name = getName(m.code)
log('Use model name from code:', m.name)
}
}
// Check if imports are string -> convert to array
if (typeof m.imports === 'string') {
m.imports = [m.imports]
}
// Infer model type
if (typeof m.type === 'undefined') {
m.type = getModelType(m)
}
// Load imports from hidden DOM element
if (m.imports && Array.isArray(m.imports) && m.imports.length) {
for (let [i, imp] of m.imports.entries()) {
if (typeof imp === 'string') {
// Convert string to object
m.imports[i] = {
url: imp
}
imp = m.imports[i]
}
if (!m.type.includes('py')) {
imp.code = utils.loadFromDOM(imp.url) // Try raw path first (matches --fetch data-src)
imp.url = utils.getUrl(imp.url) // Resolve to absolute URL for network/worker
if (!imp.code) {
imp.code = utils.loadFromDOM(imp.url) // Fallback: resolved URL (legacy compat)
}
}
}
}
// Inject CSS imports into for worker models (workers have no DOM)
if (m.imports && Array.isArray(m.imports) && m.imports.length && m.worker) {
const cssImports = m.imports.filter(imp => utils.isCssImport(imp.url))
for (const imp of cssImports) {
await utils.importScripts(imp)
}
}
console.log('Imports:', m.imports)
} // end of model-loop
log('Models initialized:', this.model.length)
}
async initInputs () {
// Check inputs
// Relies on model.code
// So run after possible fetching
if (typeof this.schema.inputs === 'undefined') {
this.model[0].container = 'args'
this.schema.inputs = getInputs(this.model[0])
}
// Read URL params, e.g. ?input1=1&input2=2
const urlParams = new URLSearchParams(window.location.search)
log('URL params:', urlParams)
// Iterate over inputs and set values from URL
this.schema.inputs.forEach(input => {
// Set default input type
if (typeof input.type === 'undefined') {
input.type = 'string'
}
// Get input value from URL params
const paramValue = utils.getUrlParam(urlParams, input)
log(`Param value for ${input.name}:`, paramValue)
if (paramValue === null) return
if (input.type === 'file') {
input.url = paramValue
input.urlAutoLoad = true
return
}
input.default = utils.coerceParam(paramValue, input.type, input.name)
})
log('Inputs are:', this.schema.inputs)
}
initVue () {
return new Promise((resolve, reject) => {
try {
log('Initializing VUE')
this.app = createVueApp(this, (container) => {
// Called when the app is mounted
// FYI "this" here refers to port object
this.outputsContainer = container.querySelector('#outputs')
this.inputsContainer = container.querySelector('#inputs')
this.modelContainer = container.querySelector('#model')
// Init overlay
this.overlay = new Overlay(this.inputsContainer ? this.inputsContainer : this.outputsContainer)
// Stop button is shown only while a run is active
this.stopElement = document.createElement('button')
this.stopElement.innerHTML = 'Stop'
this.stopElement.style = 'display: none; margin-left: 12px; background: white; color: #333; border: 1px solid #DDD; padding: 6px 10px; border-radius: 5px; cursor: pointer;'
this.stopElement.addEventListener('click', () => {
this.cancelCurrentRun()
})
this.overlay.element.appendChild(this.stopElement)
resolve()
}, log)
this.data = this.app.$data
} catch (err) {
reject(err)
}
})
}
async initPipeline () {
// Initial identity operation (just pass the input to output)
this.pipeline = (inputs) => inputs
// Async for-loop over this.model (again)
for (const [i, m] of this.model.entries()) {
let modelFunc
if (m.worker) {
// Init worker model
log(`[Init pipeline] Initializing model ${i} in a worker: ${m.name || m.url}`)
modelFunc = await this.initWorker(m)
} else {
// Init specific model types
log(`[Init pipeline] Initializing model ${i} in the main thread: ${m.name || m.url}`)
switch (m.type) {
case 'py':
modelFunc = await this.initPython(m)
break
case 'tf':
modelFunc = await this.initTF(m)
break
case 'function':
case 'class':
case 'async-init':
case 'async-function':
modelFunc = await this.initJS(m)
break
case 'get':
case 'post':
log('Initializing API model')
modelFunc = await this.initAPI(m)
break
default:
notyf.error('No type information')
throw new Error(`No type information: ${m.type}`)
}
const streamInputConfig = this.streamInputConfig || {}
const hasStreamInputs = Object.keys(streamInputConfig).length > 0
if (hasStreamInputs) {
const originalModelFunc = modelFunc
modelFunc = (inputs) => {
const wrappedInputs = utils.wrapStreamInputs(inputs, streamInputConfig, {
isCancelled: () => this.isCancelled(),
onProgress: (value) => this.progress(value)
})
return originalModelFunc(wrappedInputs)
}
}
}
this.pipeline = (p => {
return async (inputs) => {
const resPrev = await p(inputs)
// Early stop if resPrev is object and has stop flag
if (isObject(resPrev) && resPrev.stop) {
log('[Pipeline] Stopping the pipeline due to stop flag in the result')
return resPrev
}
const resNext = await modelFunc(resPrev)
if (isObject(resNext) && isObject(resPrev)) {
// If both results are objects, merge them
log(`[Pipeline] Merging results: ${Object.keys(resPrev).join(', ')} + ${Object.keys(resNext).join(', ')}`)
return Object.assign({}, resPrev, resNext)
} else if (typeof resNext !== 'undefined') {
// If next result is defined, return it
return resNext
} else {
// Otherwise return previous result (pass through)
log('[Pipeline] Passing through the previous result')
return resPrev
}
}
})(this.pipeline)
notyf.success('Pipeline initialized')
this.overlay.hide()
}
}
async initWorker (model) {
// Init worker
const worker = new Worker()
// Init worker with the model
if (typeof model.code === 'function') {
log('Convert code in schema to string for WebWorker')
model.code = model.code.toString()
}
// Wrap anonymous functions
if (!model.name) {
model.code = `function anon () { return (${model.code})(...arguments) }`
model.name = 'anon'
}
// Timeout prevents permanently frozen UI if worker hangs (default 30s, configurable via model.timeout)
const timeoutMs = model.timeout || DEFAULT_WORKER_TIMEOUT
this._cancelWorkerRun = () => worker.postMessage({ _cmd: 'cancel' })
const modelFunc = (inputs) => {
const isInitCall = inputs && inputs.code !== undefined
const payload = isInitCall
? inputs
: utils.toWorkerSerializable(inputs)
const workerPromise = new Promise((resolve, reject) => {
worker.onmessage = (e) => {
const res = e.data
if ((typeof res === 'object') && (res._status)) {
switch (res._status) {
case 'loaded':
notyf.success('Loaded model (in worker)')
log('Loaded model (in worker):', res)
this.progress(0)
resolve(res)
break
case 'log':
log(...res._log)
break
case 'progress':
this.progress(res._progress)
break
case 'error':
notyf.error(res._error)
log('Error from worker:', res._error)
this.progress(0)
reject(res._error)
break
}
} else {
log('Response from worker:', res)
this.progress(0)
resolve(res)
}
}
worker.onerror = (e) => {
notyf.error(e.message)
log('Error from worker:', e)
this.progress(0)
reject(e)
}
try {
worker.postMessage(payload)
} catch (error) {
const hasBinaryPayload = utils.containsBinaryPayload(payload)
if (hasBinaryPayload) {
const message = 'Worker postMessage failed for payload with File/Blob/binary data. JSON fallback would drop that data.'
log(message, error)
reject(new Error(message))
return
}
log('Worker postMessage failed, retrying with JSON fallback. Complex objects may lose metadata.', error)
try {
const fallbackPayload = JSON.parse(JSON.stringify(payload))
worker.postMessage(fallbackPayload)
} catch (fallbackError) {
reject(fallbackError)
}
}
})
// Skip timeout for init call (loading model can be slow); apply to execution calls
if (isInitCall) return workerPromise
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
worker.terminate()
reject(new Error(`Worker timed out after ${timeoutMs}ms`))
}, timeoutMs)
})
return Promise.race([workerPromise, timeoutPromise])
}
// Initial worker call with model definition and stream input config
const modelInitPayload = Object.assign({}, model, {
_streamInputConfig: this.streamInputConfig || {}
})
await modelFunc(modelInitPayload)
// Worker will be in the context of each modelFunc
return modelFunc
}
async initPython (model) {
// Add loading indicator
this.overlay.show()
await utils.importScripts(['https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js'])
const pyodide = await loadPyodide()
if (model.imports && Array.isArray(model.imports) && model.imports.length) {
await pyodide.loadPackage(model.imports.url)
} else {
await pyodide.loadPackagesFromImports(model.code)
}
return async (data) => {
for (let key in data) {
window[key] = data[key]
}
return await pyodide.runPythonAsync(model.code);
}
}
async initJS (model) {
// 1. String input <- loaded from url or code(string)
// 2. Target object (can be function, class or a function with async init <- code(object)
// 3. Model function
// We always start from 1 or 2
// For window execution we go: [1 ->] 2 -> 3
// For worker: [2 ->] 1 -> Worker
// Main: Initialize model in main window
// Load imports if defined (before calling the model)
if (model.imports && model.imports.length) {
log('Loading imports from schema')
await utils.importScripts(...model.imports)
notyf.success('Loaded: JS imports')
}
// Target here represents raw JS object (e.g. class), not the final callable function
let target
if (typeof model.code === 'string') {
// 1 -> 2
// Danger zone
if (model.name) {
log('Evaluating code from string (has name)')
target = Function(
`${model.code} ;return ${model.name}`
)()
} else {
log('Evaluating code from string (no name)')
target = eval(`(${model.code})`) // ( ͡° ͜ʖ ͡°) YEAHVAL
}
} else {
target = model.code
}
const modelFunc = await utils.getModelFuncJS(model, target, this)
return modelFunc
}
initAPI (model) {
log('Initializing API model:', model)
this.overlay.hide()
if (model.worker) {
// Worker:
this.worker.postMessage(model)
} else {
// Main:
return utils.getModelFuncAPI(model, log)
}
}
initTF () {
let script = document.createElement('script')
script.src = 'dist/tf.min.js'
script.onload = () => {
log('Loaded TF.js')
this.overlay.hide()
window['tf'].loadLayersModel(this.schema.model.url).then(res => {
log('Loaded Tensorflow model')
})
}
document.head.appendChild(script)
}
async run (caller='run') {
// caller can be:
// 1. custom input button name
// 2. `run`
// 3. `autorun`
// Prevent overlapping runs: autorun skips, manual clicks queue
// Prevent overlapping runs: reactive/autorun calls are dropped, manual clicks queue
if (this.running) {
if (caller === 'autorun' || caller === 'reactive') return
log('Run already in progress, queuing', caller)
this._pendingRun = caller
return
}
const schema = this.schema
const data = this.data
this.running = true
this.cancelled = false
// Run token to detect stale results when worker.onmessage gets rebound
const runToken = this._runToken = {}
try {
log('Running the pipeline...')
// Collect input values
let inputValues = {}
data.inputs.forEach(input => {
// Skip buttons
if (input.name && !(input.type == 'action' || input.type == 'button')) {
inputValues[input.name] = getValue(input)
}
})
// Add caller to input values so we can change model behavior based on it
inputValues.caller = caller
log('Input values:', inputValues)
this.overlay.show()
if (this.stopElement) {
this.stopElement.style.display = 'inline-block'
}
// Run pipeline
const results = await this.pipeline(inputValues)
// Drop stale results if a newer run started (e.g. worker.onmessage rebound race)
if (this._runToken !== runToken) return
// Output results
this.output(results)
// Check if interval is defined
if (utils.shouldContinueInterval(schema.interval, this.running, this.isCancelled(), caller)) {
log('Interval is defined:', schema.interval)
await utils.delay(schema.interval)
await this.run(caller)
}
} catch (err) {
// Surface pipeline/worker errors so they don't silently swallow failures
log('Pipeline error:', err)
notyf.error(typeof err === 'string' ? err : (err.message || 'Pipeline error'))
} finally {
// Always clean up UI state so overlay and running flag don't get stuck
this.overlay.hide()
if (this.stopElement) {
this.stopElement.style.display = 'none'
}
this.running = false
// Drain queued run if a manual click arrived while we were running
if (this._pendingRun) {
const pending = this._pendingRun
this._pendingRun = null
this.run(pending).catch(err => log('Queued run error:', err))
}
}
}
async outputAsync (res) {
this.output(res)
await utils.delay(1)
}
output (res) {
// Edge case: no output field with reactivity is handled — undefined results exit early
if (typeof res === 'undefined') {
return
}
log('[Output] Got output results of type:', typeof res)
const inputNames = this.schema.inputs ? this.schema.inputs.map(i => i.name) : []
log('Input names:', inputNames)
if (isObject(res)) {
// Drop system fields
delete res.caller
delete res.stop
delete res._status
delete res._log
delete res._progress
log('Processing results as an object:', res)
if (Object.keys(res).every(key => inputNames.includes(key))) {
// Update input fields from results
// e.g. loading a csv file and updating list of target variables
// This will be dynamically updated in the UI
log('Updating inputs from results with keys:', Object.keys(res))
this.data.inputs.forEach((input, i) => {
if (input.name && (typeof res[input.name] !== 'undefined')) {
log(`Updating input: ${input.name} with data: ${res[input.name]}`)
const r = res[input.name]
if (typeof r === 'object') {
Object.keys(r).forEach(k => {
input[k] = r[k]
})
} else {
input.value = r
}
}
})
} else if (this.data.outputs && this.data.outputs.length) {
// Update outputs from results
log('Updating outputs from results with keys:', Object.keys(res))
this.data.outputs.forEach((output, i) => {
// try output.name, sanitized output.name, output.alias
const r = res[output.name]
|| res[utils.sanitizeName(output.name)]
|| (output.alias && res[output.alias])
if (typeof r !== 'undefined') {
log(`Updating output: ${output.name} with data: ${typeof r}`)
output.value = r
}
})
} else if (!this.schema.render && !this.schema.view) {
// There's no render or view defined in the schema, also:
// No outputs defined, create outputs from results
log('Creating outputs from results with keys:', Object.keys(res))
this.data.outputs = Object.keys(res)
.filter(key => !inputNames.includes(key))
.filter(key => key !== 'caller') // Filter out caller
.map(key => {
return {
'name': key,
// typeof returns 'object' for arrays; distinguish them for proper rendering
'type': Array.isArray(res[key]) ? 'array' : typeof res[key],
'value': res[key]
}
})
}
} else if (Array.isArray(res) && res.length) {
// Result is array
if (this.data.outputs && this.data.outputs.length) {
// We have outputs defined
if (this.data.outputs.length === res.length) {
// Same length
this.data.outputs.forEach((output, i) => {
output.value = res[i]
})
} else {
// Different length
this.data.outputs[0].value = res
}
} else {
// Outputs are not defined
this.data.outputs = [{
'type': 'array',
'value': res
}]
}
} else if (this.schema.outputs && this.schema.outputs.length === 1) {
// One output value passed as raw js object
this.data.outputs[0].value = res
} else {
this.data.outputs = [{
'type': typeof res,
'value': res
}]
}
}
async download (title='output') {
// Cache the model
const clone = document.cloneNode(true)
// Change #download-btn to 'Offline: version'
const downloadBtn = clone.getElementById('download-btn')
downloadBtn.textContent = 'Offline: latest'
downloadBtn.disabled = true
downloadBtn.style.cursor = 'not-allowed'
let hiddenElement = clone.getElementById('hidden-storage');
if (!hiddenElement) {
hiddenElement = clone.createElement('div');
hiddenElement.style.display = 'none'; // Make it hidden
hiddenElement.id = 'hidden-storage'; // Assign an ID
clone.body.prepend(hiddenElement)
}
function storeInHiddenElement (url, value) {
const element = clone.createElement('script')
element.type = 'text/plain' // Make it non-executable
element.style.display = 'none' // Make it hidden
element.setAttribute('data-src', url) // Use data attribute for key
element.textContent = typeof value === 'object' ? JSON.stringify(value) : value
hiddenElement.appendChild(element)
console.log('[Hidden store] Stored:', url)
}
// Remove Google Analytics script tags
try {
clone.getElementById('ga-src').remove()
clone.getElementById('ga-body').remove()
} catch (error) {
console.error('Error removing GA script tags:', error.message)
}
console.log('Caching schema:', this.schema)
storeInHiddenElement(this.schemaUrl, this.schema)
console.log('Caching models:', this.model)
for (const model of this.model) {
storeInHiddenElement(model.url, model.code)
// Iterate over imports
if (model.imports) {
for (let imp of model.imports) {
// Store the import
const response = await fetch(imp.url)
const content = await response.text()
storeInHiddenElement(imp.url, content)
// Remove any src-based script tags with the same URL
const script = clone.querySelector('script[src="' + imp.url + '"]')
if (script) {
script.remove()
}
}
}
}
// append dummy src script for webpack fix
// const dummyScript = document.createElement('script')
// dummyScript.src = 'https://example.com/dummy.js'
// clone.body.appendChild(dummyScript)
// Find all external script tags and replace them with inline script tags
const externalScripts = Array.from(clone.querySelectorAll('script[src]'))
for (const script of externalScripts) {
try {
const response = await fetch(script.src);
if (!response.ok) throw new Error('Network response was not ok for script:' + script.src);
const content = await response.text()
const inlineScript = document.createElement('script')
inlineScript.textContent = content
inlineScript.setAttribute('data-src', script.src)
script.parentNode.replaceChild(inlineScript, script)
} catch (error) {
console.error("Error fetching script:", error.message);
}
}
// Prepare the HTML for download and trigger the download
const html = '\n' + clone.documentElement.outerHTML
console.log(html)
const blob = new Blob([html], { type: 'text/html' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = title + '.html'
a.click()
URL.revokeObjectURL(url)
}
}
=== jsee/src/overlay.js ===
class Overlay {
constructor (parent) {
this.element = document.createElement('div')
this.element.id = 'overlay'
this.element.innerHTML = `...`
parent.appendChild(this.element)
}
show () {
this.element.style.display = 'flex'
}
hide () {
this.element.style.display = 'none'
}
}
module.exports = Overlay
=== jsee/src/utils.js ===
const { DEFAULT_CHUNK_SIZE, STREAM_HIGH_WATER } = require('./constants')
// https://stackoverflow.com/questions/8511281/check-if-a-value-is-an-object-in-javascript
function isObject (item) {
return (typeof item === 'object' && !Array.isArray(item) && item !== null)
}
function shouldPreserveWorkerValue (value) {
if (!value || typeof value !== 'object') {
return true
}
if ((typeof File !== 'undefined') && (value instanceof File)) {
return true
}
if ((typeof Blob !== 'undefined') && (value instanceof Blob)) {
return true
}
if ((typeof ArrayBuffer !== 'undefined') && (value instanceof ArrayBuffer)) {
return true
}
if ((typeof ArrayBuffer !== 'undefined') && ArrayBuffer.isView && ArrayBuffer.isView(value)) {
return true
}
if ((typeof Date !== 'undefined') && (value instanceof Date)) {
return true
}
if ((typeof RegExp !== 'undefined') && (value instanceof RegExp)) {
return true
}
if ((typeof URL !== 'undefined') && (value instanceof URL)) {
return true
}
if ((typeof Map !== 'undefined') && (value instanceof Map)) {
return true
}
if ((typeof Set !== 'undefined') && (value instanceof Set)) {
return true
}
return false
}
function containsBinaryPayload (value, seen=new WeakSet()) {
if (!value || (typeof value !== 'object')) {
return false
}
if ((typeof File !== 'undefined') && (value instanceof File)) {
return true
}
if ((typeof Blob !== 'undefined') && (value instanceof Blob)) {
return true
}
if ((typeof ArrayBuffer !== 'undefined') && (value instanceof ArrayBuffer)) {
return true
}
if ((typeof ArrayBuffer !== 'undefined') && ArrayBuffer.isView && ArrayBuffer.isView(value)) {
return true
}
if (seen.has(value)) {
return false
}
seen.add(value)
if (Array.isArray(value)) {
return value.some(item => containsBinaryPayload(item, seen))
}
if ((typeof Map !== 'undefined') && (value instanceof Map)) {
for (const [key, item] of value.entries()) {
if (containsBinaryPayload(key, seen) || containsBinaryPayload(item, seen)) {
return true
}
}
return false
}
if ((typeof Set !== 'undefined') && (value instanceof Set)) {
for (const item of value.values()) {
if (containsBinaryPayload(item, seen)) {
return true
}
}
return false
}
for (const key of Object.keys(value)) {
if (containsBinaryPayload(value[key], seen)) {
return true
}
}
return false
}
const VALID_INPUT_TYPES = [
'int',
'float',
'number',
'string',
'color',
'text',
'categorical',
'select',
'bool',
'checkbox',
'file',
'group',
'action',
'button'
]
const VALID_MODEL_TYPES = [
'function',
'class',
'async-init',
'async-function',
'get',
'post',
'py',
'tf'
]
function sanitizeName (inputName) {
return inputName.toLowerCase().replace(/[^a-z0-9_]/g, '_')
}
function isWorkerInitMessage (data, initialized=false) {
if (initialized || !isObject(data)) {
return false
}
return (typeof data.url !== 'undefined') || (typeof data.code !== 'undefined')
}
function getProgressState (value) {
if (value === null) {
return {
mode: 'indeterminate',
value: null
}
}
const progressValue = Number(value)
if (Number.isNaN(progressValue)) {
return null
}
return {
mode: 'determinate',
value: Math.max(0, Math.min(100, progressValue))
}
}
function shouldContinueInterval (interval, running, cancelled, caller) {
return Boolean(interval) && running && !cancelled && caller === 'run'
}
function createAbortError (message='Operation aborted') {
const error = new Error(message)
error.name = 'AbortError'
return error
}
function isAbortRequested (signal, isCancelled) {
return !!(
(signal && signal.aborted)
|| (typeof isCancelled === 'function' && isCancelled())
)
}
function isFileLikeSource (source) {
return !!source
&& (typeof source === 'object')
&& (typeof source.slice === 'function')
&& (typeof source.size === 'number')
}
function getUrlFromSource (source) {
if (typeof source === 'string') {
return source
}
if (source && source.kind === 'url' && (typeof source.url === 'string')) {
return source.url
}
return null
}
function isChunkedReaderSource (source) {
return !!source
&& (typeof source === 'object')
&& (typeof source[Symbol.asyncIterator] === 'function')
&& (typeof source.text === 'function')
&& (typeof source.bytes === 'function')
&& (typeof source.lines === 'function')
}
function getNameFromUrl (sourceUrl) {
if ((typeof sourceUrl !== 'string') || (sourceUrl.length === 0)) {
return undefined
}
try {
const baseUrl = (typeof location !== 'undefined' && location.href)
? location.href
: 'http://localhost/'
const parsed = new URL(sourceUrl, baseUrl)
const fileName = parsed.pathname.split('/').pop()
return fileName || undefined
} catch (error) {
const noQuery = sourceUrl.split('?')[0].split('#')[0]
const fileName = noQuery.split('/').pop()
return fileName || undefined
}
}
function getStreamMetadata (source, sourceUrl) {
const metadata = {}
if (source && (typeof source === 'object')) {
if (typeof source.name === 'string') {
metadata.name = source.name
}
if ((typeof source.size === 'number') && !Number.isNaN(source.size)) {
metadata.size = source.size
}
if (typeof source.type === 'string') {
metadata.type = source.type
}
}
if (!metadata.name) {
const inferredName = getNameFromUrl(sourceUrl)
if (typeof inferredName === 'string') {
metadata.name = inferredName
}
}
return metadata
}
// Async channel with backpressure for streaming chunks
function createChunkChannel () {
const queue = []
let waitingConsumer = null // resolve fn when consumer awaits data
let waitingProducer = null // resolve fn when producer awaits drain
let done = false
let error = null
const HIGH_WATER = STREAM_HIGH_WATER
return {
async push (chunk) {
if (done || error) return
queue.push(chunk)
if (waitingConsumer) {
const resolve = waitingConsumer
waitingConsumer = null
resolve()
}
if (queue.length >= HIGH_WATER) {
await new Promise(resolve => { waitingProducer = resolve })
}
},
close () {
done = true
if (waitingConsumer) {
const resolve = waitingConsumer
waitingConsumer = null
resolve()
}
},
fail (err) {
error = err
done = true
if (waitingConsumer) {
const resolve = waitingConsumer
waitingConsumer = null
resolve()
}
},
[Symbol.asyncIterator] () {
return {
async next () {
while (queue.length === 0 && !done) {
await new Promise(resolve => { waitingConsumer = resolve })
}
if (queue.length > 0) {
const value = queue.shift()
if (waitingProducer && queue.length < HIGH_WATER) {
const resolve = waitingProducer
waitingProducer = null
resolve()
}
return { value, done: false }
}
if (error) {
throw error
}
return { value: undefined, done: true }
}
}
}
}
}
// Lightweight async-iterable reader for chunked data (File or fetch)
class ChunkedReader {
constructor (channel, metadata={}) {
this._channel = channel
if (typeof metadata.name === 'string') {
this.name = metadata.name
}
if ((typeof metadata.size === 'number') && !Number.isNaN(metadata.size)) {
this.size = metadata.size
}
if (typeof metadata.type === 'string') {
this.type = metadata.type
}
}
[Symbol.asyncIterator] () {
return this._channel[Symbol.asyncIterator]()
}
async text () {
const decoder = new TextDecoder('utf-8')
let result = ''
for await (const chunk of this) {
result += decoder.decode(chunk, { stream: true })
}
result += decoder.decode()
return result
}
async bytes () {
const parts = []
let totalLength = 0
for await (const chunk of this) {
parts.push(chunk)
totalLength += chunk.byteLength
}
const result = new Uint8Array(totalLength)
let offset = 0
for (const part of parts) {
result.set(part, offset)
offset += part.byteLength
}
return result
}
async * lines () {
const decoder = new TextDecoder('utf-8')
let remainder = ''
for await (const chunk of this) {
remainder += decoder.decode(chunk, { stream: true })
const parts = remainder.split('\n')
remainder = parts.pop()
for (const line of parts) {
yield line
}
}
remainder += decoder.decode()
if (remainder.length > 0) {
yield remainder
}
}
}
function createChunkedReader (producer, metadata={}) {
const channel = createChunkChannel()
const reader = new ChunkedReader(channel, metadata)
Promise.resolve()
.then(() => producer(
chunk => channel.push(chunk),
() => channel.close(),
err => channel.fail(err),
reader
))
.catch(err => channel.fail(err))
return reader
}
function createFileStream (source, options={}) {
const onProgress = options.onProgress
const signal = options.signal
const isCancelled = options.isCancelled
const chunkSize = (typeof options.chunkSize === 'number') && options.chunkSize > 0
? Math.floor(options.chunkSize)
: DEFAULT_CHUNK_SIZE
const readerMetadata = getStreamMetadata(source, null)
return createChunkedReader(async (pushChunk, closeStream, failStream) => {
const totalBytes = source.size
let loadedBytes = 0
const reportProgress = async (value) => {
if (typeof onProgress === 'function') {
await onProgress(value)
}
}
try {
await reportProgress(totalBytes > 0 ? 0 : null)
while (loadedBytes < totalBytes) {
if (isAbortRequested(signal, isCancelled)) {
throw createAbortError('createFileStream: aborted')
}
const nextOffset = Math.min(loadedBytes + chunkSize, totalBytes)
const blob = source.slice(loadedBytes, nextOffset)
const value = new Uint8Array(await blob.arrayBuffer())
loadedBytes = nextOffset
if (value.byteLength > 0) {
await pushChunk(value)
}
const progressValue = totalBytes > 0
? Math.round((loadedBytes / totalBytes) * 100)
: null
await reportProgress(progressValue)
}
await reportProgress(totalBytes > 0 ? 100 : null)
closeStream()
} catch (error) {
failStream(error)
}
}, readerMetadata)
}
function createFetchStream (source, options={}) {
const sourceUrl = getUrlFromSource(source)
if (!sourceUrl) {
throw new Error('createFetchStream: unsupported source type')
}
const onProgress = options.onProgress
const signal = options.signal
const isCancelled = options.isCancelled
const fetchImpl = options.fetch || (typeof fetch === 'function' ? fetch : null)
if (!fetchImpl) {
throw new Error('createFetchStream: fetch is not available')
}
const readerMetadata = getStreamMetadata(source, sourceUrl)
return createChunkedReader(async (pushChunk, closeStream, failStream, streamReader) => {
const reportProgress = async (value) => {
if (typeof onProgress === 'function') {
await onProgress(value)
}
}
const abortController = typeof AbortController !== 'undefined'
? new AbortController()
: null
const fetchSignal = abortController ? abortController.signal : signal
if (signal && abortController) {
if (signal.aborted) {
abortController.abort()
} else {
signal.addEventListener('abort', () => abortController.abort(), { once: true })
}
}
let bodyReader
try {
if (isAbortRequested(signal, isCancelled)) {
throw createAbortError('createFetchStream: aborted before fetch')
}
const response = await fetchImpl(sourceUrl, fetchSignal ? { signal: fetchSignal } : {})
if (!response.ok) {
throw new Error(`createFetchStream: failed to fetch ${sourceUrl} (${response.status})`)
}
if (!response.body) {
throw new Error(`createFetchStream: empty response body for ${sourceUrl}`)
}
const totalBytesHeader = response.headers.get('content-length')
const totalBytes = totalBytesHeader ? Number(totalBytesHeader) : null
const hasKnownLength = !!totalBytes && !Number.isNaN(totalBytes) && totalBytes > 0
if ((typeof streamReader.size !== 'number') && hasKnownLength) {
streamReader.size = totalBytes
}
const contentTypeHeader = response.headers.get('content-type')
if ((!streamReader.type) && contentTypeHeader) {
streamReader.type = contentTypeHeader.split(';')[0].trim()
}
let loadedBytes = 0
bodyReader = response.body.getReader()
await reportProgress(hasKnownLength ? 0 : null)
while (true) {
if (isAbortRequested(signal, isCancelled)) {
if (abortController) {
abortController.abort()
}
throw createAbortError('createFetchStream: aborted during read')
}
const { done, value } = await bodyReader.read()
if (done) {
break
}
loadedBytes += value.byteLength
if (value.byteLength > 0) {
await pushChunk(value)
}
const progressValue = hasKnownLength
? Math.round((loadedBytes / totalBytes) * 100)
: null
await reportProgress(progressValue)
}
await reportProgress(hasKnownLength ? 100 : null)
closeStream()
} catch (error) {
failStream(error)
} finally {
if (bodyReader) {
bodyReader.releaseLock()
}
}
}, readerMetadata)
}
function wrapStreamInputs (inputs, streamConfig={}, options={}) {
if (!isObject(inputs)) {
return inputs
}
const wrapped = Object.assign({}, inputs)
Object.keys(streamConfig).forEach((inputName) => {
const config = streamConfig[inputName]
if (!config || config.stream !== true) {
return
}
if (typeof wrapped[inputName] === 'undefined' || wrapped[inputName] === null) {
return
}
const source = wrapped[inputName]
if (isChunkedReaderSource(source)) {
return
}
if (isFileLikeSource(source)) {
wrapped[inputName] = createFileStream(source, options)
return
}
const sourceUrl = getUrlFromSource(source)
if (sourceUrl) {
wrapped[inputName] = createFetchStream(source, options)
}
})
return wrapped
}
async function getModelFuncJS (model, target, app) {
let modelFunc
switch (model.type) {
case 'class':
app.log('Init class')
const modelClass = new target()
modelFunc = (...a) => {
return modelClass[model.method || 'predict'](...a)
}
break
case 'async-init':
app.log('Function with async init')
modelFunc = await target()
break
default:
app.log('Init function')
modelFunc = target
}
// Wrap modelFunc to take into account container
// Possible cases:
if (model.container === 'args') {
return (...a) => {
if (Array.isArray(a[0]) && a[0].length && a.length === 1) {
return modelFunc(...a[0])
} else if (isObject(a[0]) && a.length === 1) {
return modelFunc(...Object.values(a[0]))
} else {
return modelFunc(...a)
}
}
} else {
return (...a) => {
if (isObject(a[0]) && a.length === 1) {
// In case when we have only one input object
// Pass log and callback to the model function
return modelFunc(a[0], app)
} else {
return modelFunc(...a)
}
}
}
}
function isCssImport (url) {
if (typeof url !== 'string') return false
const clean = url.split('?')[0].split('#')[0].toLowerCase()
return clean.endsWith('.css')
}
// Distinguish relative file paths (dist/core.js, ./lib.js) from bare package
// names (lodash, chart.js, @org/pkg). Bare names resolve to CDN; relative paths
// must resolve against the page URL so blob workers can load them.
function isRelativeImport (url) {
if (typeof url !== 'string') return false
if (url.startsWith('./') || url.startsWith('../') || url.startsWith('/')) return true
if (/^https?:\/\//i.test(url)) return false
if (url.includes('@')) return false // scoped or versioned packages (e.g. lodash@4/...)
// Bare names like "chart.js" have no slash; paths like "dist/core.js" do
if (url.includes('/') && /\.(js|css|mjs|wasm)(\?|#|$)/i.test(url)) return true
return false
}
function getUrl (url) {
let newUrl
try {
newUrl = (new URL(url)).href
} catch (e) {
if (isRelativeImport(url)) {
// Resolve against page URL so the absolute URL works inside blob workers
// (blob workers have opaque origins and can't resolve relative paths)
const base = typeof window !== 'undefined' && window.location
? window.location.href
: 'https://cdn.jsdelivr.net/npm/'
newUrl = (new URL(url, base)).href
} else {
newUrl = (new URL(url, 'https://cdn.jsdelivr.net/npm/')).href
}
}
return newUrl
}
function loadFromDOM (url) {
const scriptElement = document.querySelector(`script[data-src="${url}"]`)
if (scriptElement) {
return scriptElement.textContent
} else {
return null
}
}
function importScriptAsync (imp, async=true) {
return new Promise((resolve, reject) => {
try {
// CSS imports: inject or
{{ $parent.model.title }}
{{ $parent.model.description }}
Examples
{{ JSON.stringify(example, null, 2) }}
=== jsee/templates/bulma-input.vue ===
{{ input.title ? input.title : input.name }}
=== jsee/templates/bulma-output.vue ===
=== jsee/templates/file-picker-base.vue ===
=== jsee/templates/file-picker.vue ===
=== jsee/templates/common-inputs.js ===
const FileReader = window['FileReader']
import FilePicker from './file-picker.vue'
const component = {
props: ['input'],
emits: ['inchange'],
components: { FilePicker },
methods: {
changeHandler () {
if (this.input.reactive) {
this.$emit('inchange')
}
},
call (method) {
console.log('calling: ', method)
}
}
}
export { component }
=== jsee/templates/common-outputs.js ===
import { saveAs } from 'file-saver'
import domtoimage from 'dom-to-image'
const { sanitizeName } = require('../src/utils.js')
const Blob = window['Blob']
function stringify (v) {
return typeof v === 'string'
? v
: JSON.stringify(v)
}
const component = {
props: ['output'],
emits: ['notification'],
data () {
return {
outputName: 'output',
isFullScreen: false,
}
},
mounted() {
this.outputName = this.output.alias
? this.output.alias
: this.output.name
? sanitizeName(this.output.name)
: 'output_' + Math.floor(Math.random() * 1000000)
this.executeRenderFunction()
document.addEventListener('fullscreenchange', this.onFullScreenChange)
},
beforeUnmount() {
document.removeEventListener('fullscreenchange', this.onFullScreenChange)
},
// updated() {
// this.executeRenderFunction()
// },
watch: {
'output.value': function (newValue, oldValue) {
if (newValue !== oldValue) {
this.$nextTick(() => {
this.executeRenderFunction()
})
}
}
},
computed: {
isRenderFunction() {
return typeof this.output.value === 'function'
}
},
methods: {
toggleFullScreen() {
const el = this.$refs.cardRoot || this.$el
if (!this.isFullScreen) {
if (el.requestFullscreen) {
el.requestFullscreen()
} else if (el.webkitRequestFullscreen) {
el.webkitRequestFullscreen()
} else if (el.mozRequestFullScreen) {
el.mozRequestFullScreen()
} else if (el.msRequestFullscreen) {
el.msRequestFullscreen()
}
// state will flip in onFullScreenChange
} else {
if (document.exitFullscreen) {
document.exitFullscreen()
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen()
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen()
} else if (document.msExitFullscreen) {
document.msExitFullscreen()
}
// state will flip in onFullScreenChange
}
},
onFullScreenChange() {
this.isFullScreen = !!document.fullscreenElement
},
save () {
// Prepare filename
let filename
let extension
if (this.output.filename) {
filename = this.output.filename
} else {
let name = this.output.name ? this.output.name : 'output'
switch (this.output.type) {
case 'function':
extension = 'png'
break
case 'svg':
extension = 'svg'
break
default:
extension = 'txt'
}
filename = name + '.' + extension
}
// Prepare blob
if (this.output.type === 'function') {
domtoimage.toBlob(this.$refs.customContainer)
.then(blob => {
saveAs(blob, filename)
})
}
let value = stringify(this.output.value)
let blob = new Blob([value], {type: 'text/plain;charset=utf-8'})
saveAs(blob, filename)
},
copy () {
if (this.output.type === 'function') {
// Copy the image to the clipboard
domtoimage.toBlob(this.$refs.customContainer)
.then(blob => {
const item = new ClipboardItem({ [blob.type]: blob });
navigator.clipboard.write([item])
.then(() => {
this.$emit('notification', 'Image copied to clipboard');
})
.catch(err => {
console.error('Failed to copy image: ', err);
this.$emit('notification', 'Failed to copy image');
});
})
.catch(err => {
console.error('Failed to generate image blob: ', err);
this.$emit('notification', 'Failed to generate image');
});
} else {
let value = stringify(this.output.value)
navigator.clipboard.writeText(value)
this.$emit('notification', 'Copied')
}
},
executeRenderFunction() {
if (this.isRenderFunction && this.$refs.customContainer) {
// Clear previous content
this.$refs.customContainer.innerHTML = ''
// Execute the render function with the container
this.output.value(this.$refs.customContainer)
}
}
},
}
export { component }
=== jsee/apps/crypto_api/schema.json ===
{
"model": {
"type": "get",
"url": "https://api.coingecko.com/api/v3/coins/markets",
"worker": true,
"autorun": false
},
"inputs": [
{ "name": "vs_currency", "type": "categorical", "options": ["usd", "eur", "btc"] }
],
"outputs": [
{ "type": "object" }
]
}
=== jsee/apps/csv/schema.json ===
{
"model": {
"name": "csv",
"type": "function",
"description": "CSV file check",
"container": "args",
"url": "csv.js"
},
"inputs": [
{
"type": "file"
}
],
"outputs": [
{
"type": "file",
"filename": "output.json"
}
]
}
=== jsee/apps/deriv/schema.json ===
{
"model": [{
"name": "deriv",
"type": "function",
"url": "src/deriv.js",
"worker": false,
"autorun": true,
"imports": ["https://cdn.jsdelivr.net/npm/mathjs@latest/lib/browser/math.js"]
}],
"design": {
"grid": [4, 7]
},
"inputs": [
{ "name": "formula", "type": "string", "default": "x ^ 3", "reactive": true }
],
"outputs": [
{
"name": "hash",
"type": "text"
}
]
}
=== jsee/apps/detect/schema.json ===
{
"model": [
{
"name": "Cam",
"method": "run",
"type": "class",
"url": "cam.js",
"worker": false
},
{
"name": "CLIP",
"method": "run",
"type": "class",
"url": "clip.js",
"worker": true,
"imports": [
"https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.10.0",
"https://cdn.jsdelivr.net/npm/h5wasm@latest/dist/iife/h5wasm.js",
"https://cdn.jsdelivr.net/npm/modelzoo@latest/dist/iife/index.js"
]
},
{
"name": "View",
"method": "run",
"type": "class",
"url": "view.js",
"worker": false
}
],
"design": {
"grid": [4, 7]
},
"inputs": [
{ "name": "classes", "type": "text", "default": "man, woman, room, santa claus" },
{ "name": "detect", "type": "text", "default": "santa claus" },
{ "name": "load", "type": "action", "display": true, "title": "Load model"}
],
"outputs": [
{
"name": "output",
"type": "text"
}
],
"examples": [
{ "classes": "man, woman, room, santa claus", "detect": "santa claus" },
{ "classes": "street, car, bird, dog", "detect": "bird"}
],
"interval": 1000
}
=== jsee/apps/ethgen/schema.json ===
{
"model": {
"name": "ethgen",
"type": "function",
"url": "bundle.js",
"worker": true,
"autorun": true
},
"design": {
"grid": [2, 10]
},
"outputs": [
{ "name": "Address", "type": "code", "filename": "eth.pub" },
{ "name": "Private key", "type": "code", "filename": "eth.key" }
]
}
=== jsee/apps/factorial/schema.json ===
{
"model": {
"name": "factorial",
"type": "function",
"description": "Javascript function that computes factorial. Nothing special",
"container": "args",
"url": "factorial.js",
"autorun": true
},
"inputs": [
{
"type": "int",
"min": 0
}
],
"outputs": [
{
"type": "int"
}
]
}
=== jsee/apps/git2pdf/schema.json ===
{
"model": [
{
"name": "git2pdf",
"type": "function",
"url": "git2pdf.js",
"worker": false,
"imports": [
"https://unpkg.com/@isomorphic-git/lightning-fs",
"https://unpkg.com/isomorphic-git@beta",
"https://unpkg.com/isomorphic-git@beta/http/web/index.umd.js"
]
}
],
"design": {
"grid": [4, 7]
},
"inputs": [
{ "name": "repo", "type": "text", "default": "https://github.com/jseeio/jsee" },
{ "name": "corsProxy", "type": "text", "default": "https://cors.isomorphic-git.org" },
{ "name": "extensions", "type": "text", "default": "py,js,md" }
],
"outputs": [
{
"name": "output",
"type": "text"
}
]
}
=== jsee/apps/gpt2/schema.json ===
{
"model": {
"name": "gpt2",
"type": "function",
"url": "https://cdn.jsdelivr.net/npm/@jseeio/gpt2-tfjs@latest/bundle.js",
"worker": false,
"autorun": false
},
"design": {
"grid": [4, 6]
},
"inputs": [
{ "name": "model", "type": "select", "options": ["gpt2", "gpt2-medium", "gpt2-large"], "default": "gpt2"},
{ "name": "maxLength", "type": "int", "default": 50},
{ "name": "temperature", "type": "float", "default": 1, "min": 0, "max": 1},
{ "name": "input", "type": "text", "default": "The quick brown fox jumps over the lazy dog." }
],
"outputs": [
{
"name": "output",
"type": "text"
}
],
"examples": [
{ "input": "My name is Anton and I am" },
{ "input": "import matplotlib.pyplot as" },
{ "input": "The quick brown fox jumps over the lazy dog" }
]
}
=== jsee/apps/hashr/schema.json ===
{
"model": {
"name": "deriv",
"type": "function",
"url": "src/deriv.js",
"worker": true,
"autorun": true,
"imports": ["https://cdn.jsdelivr.net/npm/mathjs@latest/lib/browser/math.js"]
},
"design": {
"grid": [4, 7]
},
"inputs": [
{ "name": "method", "type": "select", "options": ["sha1", "sha224", "sha256", "sha384", "sha512"], "default": "sha256"},
{ "name": "file", "type": "file" },
{ "name": "formula", "type": "string", "default": "x ^ 3", "reactive": true }
],
"outputs": [
{
"name": "hash",
"type": "text"
}
]
}
=== jsee/apps/hash/schema.json ===
{
"model": {
"name": "hash",
"type": "function",
"url": "hash.js",
"worker": true,
"autorun": true
},
"design": {
"grid": [4, 8]
},
"inputs": [
{ "name": "method", "type": "select", "options": ["sha1", "sha224", "sha256", "sha384", "sha512"], "default": "sha256"},
{ "name": "file", "type": "file" }
],
"outputs": [
{
"name": "hash",
"type": "text"
}
]
}
=== jsee/apps/keygen/schema.json ===
{
"model": {
"name": "keygen",
"type": "async-function",
"url": "index.js",
"worker": false,
"autorun": false
},
"design": {
"grid": [4, 8]
},
"inputs": [
{ "name": "length", "type": "categorical", "options": [1024, 2048, 4096], "default": 4096 },
{ "name": "hash", "type": "categorical", "options": ["SHA-1", "SHA-256", "SHA-384", "SHA-512"], "default": "SHA-512" }
],
"outputs": [
{ "name": "Public key", "type": "code", "filename": "id_rsa.pub" },
{ "name": "Private key", "type": "code", "filename": "id_rsa" }
]
}
=== jsee/apps/pendulum/schema.json ===
{
"model": {
"name": "Pendulum",
"method": "main",
"type": "class",
"url": "pendulum.js",
"worker": false,
"autorun": true
},
"design": {
"grid": [3, 9]
},
"inputs": [
{ "name": "n_layers", "type": "int", "default": 2, "min": 1, "max": 4 },
{ "name": "n_units", "type": "categorical", "options": [8, 16, 32, 64], "default": 32 },
{ "name": "n_epochs", "type": "int", "default": 50, "min": 1, "max": 200 },
{ "name": "timesteps_rollout", "type": "int", "default": 1024 },
{ "name": "timesteps_total", "type": "int", "default": 10240 },
{ "name": "training", "type": "bool", "default": true },
{ "name": "load_db", "type": "action", "title": "Load from IndexedDB"},
{ "name": "save_db", "type": "action", "display": false, "title": "Save to IndexedDB"},
{ "name": "load_disk", "type": "action", "title": "Load from Disk"},
{ "name": "save_disk", "type": "action", "display": false, "title": "Save to Disk"}
],
"imports": [
"https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest",
"https://cdn.jsdelivr.net/npm/matter-js@latest",
"https://cdn.jsdelivr.net/npm/ppo-tfjs@latest"
]
}
=== jsee/apps/pyselection/schema.json ===
{
"model": {
"name": "pyselection",
"type": "py",
"description": "Load Pyboruta and run feature selection",
"container": "object",
"url": "pyselection.py"
},
"inputs": [
{
"type": "string",
"name": "target"
},
{
"type": "file",
"name": "csv"
}
]
}
=== jsee/apps/pysummary/schema.json ===
{
"model": {
"name": "pysummary",
"type": "py",
"description": "Make pandas generate summary of csv file",
"container": "object",
"url": "pysummary.py"
},
"inputs": [
{
"type": "file",
"name": "csv"
}
],
"outputs": [
{
"type": "object",
"name": "res"
}
]
}
=== jsee/apps/pytest/schema.json ===
{
"model": {
"name": "pytest",
"type": "py",
"description": "Testing pyodide project",
"container": "object",
"url": "pytest.py",
"autorun": true
},
"inputs": [
{
"type": "int",
"name": "a"
},
{
"type": "int",
"name": "b"
}
],
"outputs": [
{
"type": "int"
}
]
}
=== jsee/apps/qrcode/schema.json ===
{
"model": {
"name": "QRCode",
"type": "function",
"title": "QR Code generator",
"description": "Fast customizable QR code generator. Based on the pure-svg-code package ",
"container": "object",
"url": "qrcode.js",
"autorun": true
},
"inputs": [
{ "name": "content", "type": "text", "default": "https://statsim.com", "reactive": true },
{ "name": "size", "type": "int" },
{ "name": "color", "type": "string" }
],
"outputs": [
{ "name": "qrcode", "type": "svg" }
]
}
=== jsee/apps/sentiment/schema.json ===
{
"model": {
"name": "Sentiment",
"type": "class",
"method": "analyze",
"title": "Sentiment analysis",
"description": "AFINN-based sentiment analysis",
"container": "args",
"url": "sentiment.js"
},
"design": {
"layout": "sidebar",
"colors": "light"
},
"inputs": [
{
"type": "text"
}
],
"outputs": [
{
"name": "score",
"type": "int"
},
{
"name": "positive",
"type": "array"
},
{
"name": "negative",
"type": "array"
}
]
}
=== jsee/apps/spurious/schema.json ===
{
"model": {
"type": "class",
"name": "Spurious",
"method": "run",
"url": "spurious.js",
"worker": false
},
"design": {
"layout": "sidebar",
"colors": "light"
},
"inputs": [
{
"name": "n",
"type": "int",
"min": 10,
"max": 10000,
"default": 1000
}
]
}
=== jsee/apps/test_cli/schema.json ===
{
"model": {
"name": "model",
"type": "function",
"url": "dist/model.js",
"worker": true
},
"render": {
"name": "view",
"type": "function",
"url": "dist/view.js",
"worker": false
},
"design": {
"layout": "sidebar",
"colors": "light"
},
"inputs": [
{ "type": "file", "name": "file", "reactive": true, "alias": "f" },
{ "type": "int", "name": "topN", "default": 10, "alias": "n", "title": "Top N" }
],
"autorun": true,
"interval": 2000,
"debug": true
}
=== jsee/apps/test_layout/schema.json ===
{
"model": {
"name": "test",
"type": "function",
"title": "QR Code generator",
"description": "Fast customizable QR code generator. Based on the pure-svg-code package ",
"container": "object",
"url": "main.js",
"autorun": true
},
"design": {
"grid": [12, 12]
},
"inputs": [
{ "name": "order", "type": "group", "elements": [
{ "name": "p", "type": "int", "reactive": true, "group": "params", "value": 0 },
{ "name": "d", "type": "int", "reactive": true, "group": "params", "value": 0 },
{ "name": "q", "type": "int", "reactive": true, "group": "params", "value": 0 }
] }
],
"outputs": [
{ "name": "result", "type": "object" }
]
}
=== jsee/apps/test_object/schema.json ===
{
"model": {
"name": "test",
"type": "function",
"title": "QR Code generator",
"description": "Fast customizable QR code generator. Based on the pure-svg-code package ",
"container": "object",
"url": "main.js",
"autorun": true
},
"inputs": [
{ "name": "x" },
{ "name": "y" }
],
"outputs": [
{ "name": "result", "type": "object" }
]
}
=== jsee/apps/test_qr/schema.json ===
{
"model": {
"name": "QRCode",
"type": "function",
"title": "QR Code generator",
"description": "Fast customizable QR code generator. Based on the pure-svg-code package ",
"container": "object",
"url": "qrcode.js",
"autorun": false
},
"inputs": [
{ "name": "testint", "type": "int", "reactive": true},
{ "name": "content", "type": "text", "default": "https://statsim.com", "placeholder": "Content for QR code" },
{ "name": "size", "type": "int", "display": "color == '6'"},
{ "name": "color", "type": "color", "placeholder": "E.g. #000077" },
{ "name": "rnd", "type": "bool" },
{ "name": "rnd2", "type": "select", "options": ["a", "b"], "default": "b" },
{ "name": "rnd3", "type": "file", "reactive": true },
{ "name": "order", "type": "group", "elements": [
{ "name": "p", "type": "int", "reactive": true, "group": "params", "value": 0 },
{ "name": "d", "type": "int", "reactive": true, "group": "params", "value": 0 },
{ "name": "q", "type": "int", "reactive": true, "group": "params", "value": 0 }
] },
{ "name": "seasonal_order", "type": "group", "elements": [
{ "name": "P", "type": "int", "reactive": true, "group": "params", "value": 0 },
{ "name": "D", "type": "int", "reactive": true, "group": "params", "value": 0 },
{ "name": "Q", "type": "int", "reactive": true, "group": "params", "value": 0 },
{ "name": "s", "type": "int", "reactive": true, "group": "params", "value": 0 }
] }
],
"outputs": [
{ "name": "qrcode", "type": "svg" }
]
}
=== jsee/apps/test_render/schema.json ===
{
"model": {
"name": "test",
"type": "function",
"title": "QR Code generator",
"description": "Fast customizable QR code generator. Based on the pure-svg-code package ",
"container": "object",
"url": "main.js",
"autorun": true,
"worker": false,
"imports": [
"https://cdn.jsdelivr.net/npm/d3@7",
"https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6"
]
},
"design": {
"layout": "sidebar",
"colors": "light",
"grid": [12, 12]
},
"inputs": [
{ "name": "x", "columns": 4, "reactive": true },
{ "name": "y", "columns": 4, "reactive": true },
{ "name": "z", "columns": 4, "reactive": true }
],
"outputs": [
{ "name": "A B", "type": "function", "columns": 8 },
{ "name": "result1", "type": "object", "columns": 4 },
{ "name": "result2", "type": "object", "columns": 4 },
{ "name": "result3", "type": "function", "columns": 8 }
]
}
=== jsee/apps/test/schema.json ===
{
"model": {
"name": "pow",
"type": "function",
"container": "args",
"url": "pow.js",
"autorun": true
},
"inputs": [
{
"type": "int",
"min": 4
}
],
"outputs": [
{
"type": "int"
}
]
}
=== jsee/apps/ulam/schema.json ===
{
"model": {
"type": "class",
"name": "PrimeSpiral",
"method": "run",
"url": "main.js",
"worker": false,
"autorun": true
},
"design": {
"grid": [3, 7]
},
"inputs": [
{
"name": "n",
"type": "int",
"min": 10,
"max": 1000000,
"default": 100000
},
{
"name": "scale",
"type": "int",
"min": 1,
"max": 1000,
"default": 100
},
{
"name": "pointSize",
"type": "int",
"min": 1,
"max": 100,
"default": 10
}
]
}