@jseeio/jsee 0.3.7 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +9 -0
- package/AGENTS.md +37 -0
- package/CHANGELOG.md +71 -0
- package/CLAUDE.md +5 -0
- package/README.md +20 -41
- package/bin/jsee +1 -1
- package/dist/jsee.js +1 -1
- package/dist/jsee.runtime.js +1 -1
- package/jest-puppeteer.config.js +2 -1
- package/jest.unit.config.js +8 -0
- package/load/index.html +9 -4
- package/package.json +15 -13
- package/src/app.js +35 -11
- package/src/cli.js +662 -548
- package/src/main.js +348 -152
- package/src/utils.js +590 -3
- package/src/worker.js +42 -18
- package/templates/bulma-app.vue +3 -2
- package/templates/bulma-input.vue +22 -18
- package/templates/bulma-output.vue +72 -7
- package/templates/common-inputs.js +2 -13
- package/templates/common-outputs.js +57 -2
- package/templates/file-picker-base.vue +169 -0
- package/templates/file-picker.vue +318 -0
- package/test/fixtures/lodash-like.js +15 -0
- package/test/fixtures/upload-sample.csv +3 -0
- package/test/test-basic.test.js +286 -11
- package/test/unit/utils.test.js +519 -0
- package/webpack.config.js +1 -0
package/src/cli.js
CHANGED
|
@@ -20,6 +20,8 @@ const converter = new showdown.Converter({
|
|
|
20
20
|
})
|
|
21
21
|
showdown.setFlavor('github')
|
|
22
22
|
|
|
23
|
+
const { getModelFuncJS, sanitizeName } = require('./utils.js')
|
|
24
|
+
|
|
23
25
|
// left padding of multiple lines
|
|
24
26
|
function pad (str, len, start=0) {
|
|
25
27
|
return str.split('\n').map((s, i) => i >= start ? ' '.repeat(len) + s : s).join('\n')
|
|
@@ -29,585 +31,261 @@ function depad (str, len) {
|
|
|
29
31
|
return str.split('\n').map(s => s.slice(len)).join('\n')
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
34
|
+
function getDataFromArgv (schema, argv, loadFiles=true) {
|
|
35
|
+
let data = {}
|
|
36
|
+
if (schema.inputs) {
|
|
37
|
+
schema.inputs.forEach(inp => {
|
|
38
|
+
const inputName = sanitizeName(inp.name)
|
|
39
|
+
console.log('Processing input:', inp.name, 'as', inputName)
|
|
40
|
+
if (inputName in argv) {
|
|
41
|
+
switch (inp.type) {
|
|
42
|
+
case 'file':
|
|
43
|
+
if (!loadFiles) {
|
|
44
|
+
// If we don't want to load files, just set the value to the file path
|
|
45
|
+
data[inp.name] = argv[inputName]
|
|
46
|
+
break
|
|
47
|
+
} else if (fs.existsSync(argv[inputName])) {
|
|
48
|
+
data[inp.name] = fs.readFileSync(argv[inputName], 'utf8')
|
|
49
|
+
} else {
|
|
50
|
+
console.error(`File not found: ${argv[inputName]}`)
|
|
51
|
+
process.exit(1)
|
|
52
|
+
}
|
|
53
|
+
break
|
|
54
|
+
case 'int':
|
|
55
|
+
data[inp.name] = parseInt(argv[inputName], 10)
|
|
56
|
+
break
|
|
57
|
+
case 'float':
|
|
58
|
+
data[inp.name] = parseFloat(argv[inputName])
|
|
59
|
+
break
|
|
60
|
+
case 'string':
|
|
61
|
+
default:
|
|
62
|
+
data[inp.name] = argv[inputName]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
})
|
|
58
66
|
}
|
|
67
|
+
return data
|
|
68
|
+
}
|
|
59
69
|
|
|
60
|
-
|
|
61
|
-
let
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// Set argv.inputs to the first non-option argument if it exists
|
|
67
|
-
if (argv._.length > 0) {
|
|
68
|
-
argv.inputs = argv._[0]
|
|
70
|
+
function genSchema (jsdocData) {
|
|
71
|
+
let schema = {
|
|
72
|
+
model: [],
|
|
73
|
+
inputs: [],
|
|
74
|
+
outputs: [],
|
|
69
75
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
76
|
+
for (let d of jsdocData) {
|
|
77
|
+
const model = {
|
|
78
|
+
name: d.name ? d.name : d.meta.filename.split('.')[0],
|
|
79
|
+
description: d.description ? d.description : '',
|
|
80
|
+
type: d.kind,
|
|
81
|
+
container: 'args',
|
|
82
|
+
url: path.relative(process.cwd(), path.join(d.meta.path, d.meta.filename)),
|
|
83
|
+
worker: false
|
|
84
|
+
}
|
|
85
|
+
if (d.requires) {
|
|
86
|
+
model.imports = d.requires.map(r => r.replace('module:', ''))
|
|
87
|
+
}
|
|
88
|
+
if (d.params) {
|
|
89
|
+
// Check if all params have the same name before '.'
|
|
90
|
+
const names = new Set(d.params.map(p => p.name.split('.')[0]))
|
|
91
|
+
if ((d.params.length > 1) && (names.size === 1)) {
|
|
92
|
+
// Object
|
|
93
|
+
model.container = 'object'
|
|
94
|
+
d.params.slice(1).forEach(p => {
|
|
95
|
+
const inp = {
|
|
96
|
+
name: p.name.split('.')[1],
|
|
97
|
+
type: p.type.names[0],
|
|
98
|
+
description: p.description,
|
|
99
|
+
}
|
|
100
|
+
if (p.defaultvalue) {
|
|
101
|
+
inp.default = p.defaultvalue
|
|
102
|
+
}
|
|
103
|
+
schema.inputs.push(inp)
|
|
104
|
+
})
|
|
105
|
+
} else {
|
|
106
|
+
// Array
|
|
107
|
+
model.container = 'args'
|
|
108
|
+
d.params.forEach(p => {
|
|
109
|
+
const inp = {
|
|
110
|
+
name: p.name,
|
|
111
|
+
type: p.type.names[0],
|
|
112
|
+
description: p.description,
|
|
113
|
+
}
|
|
114
|
+
if (p.defaultvalue) {
|
|
115
|
+
inp.default = p.defaultvalue
|
|
116
|
+
}
|
|
117
|
+
schema.inputs.push(inp)
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (d.returns) {
|
|
122
|
+
d.returns.forEach(r => {
|
|
123
|
+
r.name = r.name ? r.name : r.description.split('-')[0].trim()
|
|
124
|
+
r.description = r.description.split('-').slice(1).join('-').trim()
|
|
125
|
+
})
|
|
126
|
+
const names = new Set(d.returns.map(r => r.name.split('.')[0]))
|
|
127
|
+
if ((d.returns.length > 1) && (names.size === 1)) {
|
|
128
|
+
// Object
|
|
129
|
+
d.returns.slice(1).forEach(p => {
|
|
130
|
+
const out = {
|
|
131
|
+
name: p.name.split('.')[1],
|
|
132
|
+
type: p.type.names[0],
|
|
133
|
+
description: p.description,
|
|
134
|
+
}
|
|
135
|
+
schema.outputs.push(out)
|
|
136
|
+
})
|
|
137
|
+
} else {
|
|
138
|
+
// Array
|
|
139
|
+
d.returns.forEach(p => {
|
|
140
|
+
const out = {
|
|
141
|
+
name: p.name,
|
|
142
|
+
type: p.type.names[0],
|
|
143
|
+
description: p.description,
|
|
144
|
+
}
|
|
145
|
+
schema.outputs.push(out)
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (d.customTags) {
|
|
150
|
+
d.customTags.forEach(t => {
|
|
151
|
+
if (t.tag === 'worker') {
|
|
152
|
+
model.worker = true
|
|
153
|
+
}
|
|
154
|
+
})
|
|
89
155
|
}
|
|
156
|
+
schema.model.push(model)
|
|
90
157
|
}
|
|
158
|
+
return schema
|
|
159
|
+
}
|
|
91
160
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
outputs = outputs.split(',')
|
|
95
|
-
}
|
|
161
|
+
function genHtmlFromSchema(schema) {
|
|
162
|
+
let htmlDescription = '<br><div class="schema-description">';
|
|
96
163
|
|
|
97
|
-
//
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
164
|
+
// Process the model section
|
|
165
|
+
if (schema.model && schema.model.length > 0) {
|
|
166
|
+
schema.model.forEach(model => {
|
|
167
|
+
htmlDescription += `<h3><strong>${model.name}</strong></h3>`
|
|
168
|
+
if (model.description) {
|
|
169
|
+
htmlDescription += `<p>${model.description}</p>`
|
|
170
|
+
}
|
|
171
|
+
})
|
|
101
172
|
}
|
|
102
173
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// switch to fs.readFileSync to reload the schema if it changes
|
|
111
|
-
schema = JSON.parse(fs.readFileSync(path.join(cwd, inputs[0]), 'utf8'))
|
|
112
|
-
} else {
|
|
113
|
-
// Array of js files
|
|
114
|
-
// Generate schema
|
|
115
|
-
let jsdocData = jsdoc2md.getTemplateDataSync({ files: inputs.map(f => path.join(cwd, f)) })
|
|
116
|
-
schema = genSchema(jsdocData)
|
|
117
|
-
// jsdocMarkdown = jsdoc2md.renderSync({
|
|
118
|
-
// data: jsdocData,
|
|
119
|
-
// 'param-list-format': 'list',
|
|
120
|
-
// })
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Iterate over schema inputs and update aliases
|
|
124
|
-
if (schema.inputs) {
|
|
125
|
-
schema.inputs.forEach(inp => {
|
|
126
|
-
if (inp.name) {
|
|
127
|
-
if (inp.alias) {
|
|
128
|
-
argvAlias[inp.name] = inp.alias
|
|
129
|
-
}
|
|
130
|
-
if (inp.default) {
|
|
131
|
-
argvDefault[inp.name] = inp.default
|
|
132
|
-
}
|
|
174
|
+
// Process the inputs section
|
|
175
|
+
if (schema.inputs && schema.inputs.length > 0) {
|
|
176
|
+
htmlDescription += '<h4>Inputs</h4><ul>';
|
|
177
|
+
schema.inputs.forEach(input => {
|
|
178
|
+
htmlDescription += `<li><strong>${input.name}</strong> (${input.type})`
|
|
179
|
+
if (input.description) {
|
|
180
|
+
htmlDescription += ` - ${input.description}`
|
|
133
181
|
}
|
|
134
182
|
})
|
|
183
|
+
htmlDescription += '</ul>';
|
|
135
184
|
}
|
|
136
185
|
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// Initially in argv.fetch branch
|
|
146
|
-
// Check if schema has model, convert to array if needed
|
|
147
|
-
if (!schema.model) {
|
|
148
|
-
console.error('No model found in schema')
|
|
149
|
-
process.exit(1)
|
|
150
|
-
}
|
|
151
|
-
if (!Array.isArray(schema.model)) {
|
|
152
|
-
schema.model = [schema.model]
|
|
153
|
-
}
|
|
154
|
-
if (argv.execute) {
|
|
155
|
-
schema.model.forEach(m => {
|
|
156
|
-
console.log('Executing model on the server:', m.name)
|
|
157
|
-
modelFuncs[m.name] = require(path.join(cwd, m.url))
|
|
158
|
-
m.type = 'post'
|
|
159
|
-
m.url = `/${m.name}`
|
|
160
|
-
m.worker = false
|
|
186
|
+
// Process the outputs section
|
|
187
|
+
if (schema.outputs && schema.outputs.length > 0) {
|
|
188
|
+
htmlDescription += '<h4>Outputs</h4><ul>';
|
|
189
|
+
schema.outputs.forEach(output => {
|
|
190
|
+
htmlDescription += `<li><strong>${output.name}</strong> (${output.type})`
|
|
191
|
+
if (output.description) {
|
|
192
|
+
htmlDescription += ` - ${output.description}`
|
|
193
|
+
}
|
|
161
194
|
})
|
|
195
|
+
htmlDescription += '</ul>';
|
|
162
196
|
}
|
|
163
|
-
|
|
164
|
-
|
|
197
|
+
htmlDescription += '</div>';
|
|
198
|
+
return htmlDescription;
|
|
199
|
+
}
|
|
165
200
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const descriptionMd = fs.readFileSync(path.join(cwd, description), 'utf8')
|
|
169
|
-
descriptionHtml = converter.makeHtml(descriptionMd)
|
|
201
|
+
function genMarkdownFromSchema(schema) {
|
|
202
|
+
let markdownDescription = '';
|
|
170
203
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
204
|
+
// Process the model section
|
|
205
|
+
if (schema.model && schema.model.length > 0) {
|
|
206
|
+
schema.model.forEach(model => {
|
|
207
|
+
markdownDescription += `### **${model.name}**\n`;
|
|
208
|
+
if (model.description) {
|
|
209
|
+
markdownDescription += `${model.description}\n\n`;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
180
212
|
}
|
|
181
213
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
214
|
+
// Process the inputs section
|
|
215
|
+
if (schema.inputs && schema.inputs.length > 0) {
|
|
216
|
+
markdownDescription += '#### Inputs\n';
|
|
217
|
+
schema.inputs.forEach(input => {
|
|
218
|
+
markdownDescription += `- **${input.name}** (${input.type})`;
|
|
219
|
+
if (input.description) {
|
|
220
|
+
markdownDescription += ` - ${input.description}`;
|
|
221
|
+
}
|
|
222
|
+
markdownDescription += '\n';
|
|
223
|
+
});
|
|
224
|
+
}
|
|
185
225
|
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
jseeCode = fs.readFileSync(path.join(__dirname, '..', 'dist', 'jsee.js'), 'utf8')
|
|
194
|
-
} else if (argv.version === 'latest') {
|
|
195
|
-
jseeCode = fs.readFileSync(path.join(__dirname, '..', 'dist', 'jsee.runtime.js'), 'utf8')
|
|
196
|
-
} else {
|
|
197
|
-
// Pre-fetch the jsee runtime from the CDN https://cdn.jsdelivr.net/npm/@jseeio/jsee@${argv.version}/dist/jsee.runtime.js
|
|
198
|
-
jseeCode = await fetch(`https://cdn.jsdelivr.net/npm/@jseeio/jsee@${argv.version}/dist/jsee.runtime.js`)
|
|
199
|
-
jseeCode = await jseeCode.text()
|
|
200
|
-
}
|
|
201
|
-
jseeHtml = `<script>${jseeCode}</script>`
|
|
202
|
-
// Fetch model files and store them in hidden elements
|
|
203
|
-
hiddenElementHtml += '<div id="hidden-storage" style="display: none;">'
|
|
204
|
-
|
|
205
|
-
for (let m of schema.model) {
|
|
206
|
-
if (m.type === 'get' || m.type === 'post') {
|
|
207
|
-
continue
|
|
208
|
-
}
|
|
209
|
-
if (m.url) {
|
|
210
|
-
const modelCode = fs.readFileSync(path.join(cwd, m.url), 'utf8')
|
|
211
|
-
hiddenElementHtml += `<script type="text/plain" style="display: none;" data-src="${m.url}">${modelCode}</script>`
|
|
226
|
+
// Process the outputs section
|
|
227
|
+
if (schema.outputs && schema.outputs.length > 0) {
|
|
228
|
+
markdownDescription += '#### Outputs\n';
|
|
229
|
+
schema.outputs.forEach(output => {
|
|
230
|
+
markdownDescription += `- **${output.name}** (${output.type})`;
|
|
231
|
+
if (output.description) {
|
|
232
|
+
markdownDescription += ` - ${output.description}`;
|
|
212
233
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
// Create cache directory if it doesn't exist
|
|
218
|
-
const cacheDir = path.join(os.homedir(), '.cache', 'jsee')
|
|
219
|
-
fs.mkdirSync(cacheDir, { recursive: true })
|
|
220
|
-
|
|
221
|
-
// Create a hash of the importUrl
|
|
222
|
-
const hash = crypto.createHash('sha256').update(importUrl).digest('hex')
|
|
223
|
-
const cacheFilePath = path.join(cacheDir, `${hash}.js`)
|
|
224
|
-
|
|
225
|
-
let importCode
|
|
226
|
-
let useCache = false
|
|
227
|
-
|
|
228
|
-
// Check if cache file exists and is less than 1 day old
|
|
229
|
-
if (fs.existsSync(cacheFilePath)) {
|
|
230
|
-
const stats = fs.statSync(cacheFilePath)
|
|
231
|
-
const mtime = new Date(stats.mtime)
|
|
232
|
-
const now = new Date()
|
|
233
|
-
const ageInDays = (now - mtime) / (1000 * 60 * 60 * 24)
|
|
234
|
+
markdownDescription += '\n';
|
|
235
|
+
});
|
|
236
|
+
}
|
|
234
237
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
importCode = fs.readFileSync(cacheFilePath, 'utf8');
|
|
238
|
-
useCache = true;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
238
|
+
return markdownDescription;
|
|
239
|
+
}
|
|
241
240
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
241
|
+
function template(schema, blocks) {
|
|
242
|
+
let title = 'jsee'
|
|
243
|
+
// let url = schema.page && schema.page.url ? schema.page.url : ''
|
|
244
|
+
let url = ('page' in schema && 'url' in schema.page) ? schema.page.url : ''
|
|
245
|
+
if (schema.title) {
|
|
246
|
+
title = schema.title
|
|
247
|
+
} else if (schema.page && schema.page.title) {
|
|
248
|
+
title = schema.page.title
|
|
249
|
+
} else if (schema.model) {
|
|
250
|
+
if (Array.isArray(schema.model)) {
|
|
251
|
+
title = schema.model[0].name
|
|
252
|
+
} else {
|
|
253
|
+
title = schema.model.name
|
|
255
254
|
}
|
|
256
|
-
hiddenElementHtml += '</div>'
|
|
257
|
-
} else {
|
|
258
|
-
jseeHtml = outputs
|
|
259
|
-
? `<script src="https://cdn.jsdelivr.net/npm/@jseeio/jsee@${argv.version}/dist/jsee.runtime.js"></script>`
|
|
260
|
-
: `<script src="http://localhost:${argv.port}/dist/jsee.runtime.js"></script>`
|
|
261
255
|
}
|
|
262
256
|
|
|
263
|
-
|
|
264
|
-
let gaHtml = ''
|
|
265
|
-
let orgHtml = ''
|
|
257
|
+
return `<!DOCTYPE html>
|
|
266
258
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
if (schema.page.ga) {
|
|
272
|
-
gaHtml = `
|
|
273
|
-
<script id="ga-src" async src="https://www.googletagmanager.com/gtag/js?id=${schema.page.ga}"></script>
|
|
274
|
-
<script id="ga-body">
|
|
275
|
-
window['ga-disable-${schema.page.ga}'] = window.doNotTrack === "1" || navigator.doNotTrack === "1" || navigator.doNotTrack === "yes" || navigator.msDoNotTrack === "1";
|
|
276
|
-
window.dataLayer = window.dataLayer || [];
|
|
277
|
-
function gtag(){dataLayer.push(arguments);}
|
|
278
|
-
gtag('js', new Date());
|
|
279
|
-
gtag('config', '${schema.page.ga}');
|
|
280
|
-
</script>
|
|
281
|
-
`
|
|
282
|
-
}
|
|
259
|
+
<!-- Generated by JSEE (https://jsee.org) -->
|
|
260
|
+
<!-- Do not edit this file directly. Edit the source files and run jsee to generate this file. -->
|
|
261
|
+
<!-- License: MIT (https://opensource.org/licenses/MIT) -->
|
|
283
262
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
break
|
|
292
|
-
case 'github':
|
|
293
|
-
socialHtml += `<li><a rel="me" href="https://github.com/${url}">GitHub</a></li>`
|
|
294
|
-
break
|
|
295
|
-
case 'facebook':
|
|
296
|
-
socialHtml += `<li><a rel="me" href="https://www.facebook.com/${url}">Facebook</a></li>`
|
|
297
|
-
break
|
|
298
|
-
case 'linkedin':
|
|
299
|
-
socialHtml += `<li><a rel="me" href="https://www.linkedin.com/company/${url}">LinkedIn</a></li>`
|
|
300
|
-
break
|
|
301
|
-
case 'instagram':
|
|
302
|
-
socialHtml += `<li><a rel="me" href="https://www.instagram.com/${url}">Instagram</a></li>`
|
|
303
|
-
break
|
|
304
|
-
case 'youtube':
|
|
305
|
-
socialHtml += `<li><a rel="me" href="https://www.youtube.com/${url}">YouTube</a></li>`
|
|
306
|
-
break
|
|
307
|
-
default:
|
|
308
|
-
socialHtml += `<li><a rel="me" href="${s.url}">${s.name}</a></li>`
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
263
|
+
<html lang="en">
|
|
264
|
+
<head>
|
|
265
|
+
<meta charset="utf-8">
|
|
266
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
267
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
268
|
+
<title>${title}</title>
|
|
269
|
+
<meta name="description" content="${blocks.descriptionTxt}">
|
|
312
270
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
271
|
+
<!-- Open Graph -->
|
|
272
|
+
<meta property="og:title" content="${title}" />
|
|
273
|
+
<meta property="og:description" content="${blocks.descriptionTxt}" />
|
|
274
|
+
<meta property="og:locale" content="en_US" />
|
|
275
|
+
<meta property="og:url" content="${url}" />
|
|
276
|
+
<meta property="og:site_name" content="${title}" />
|
|
277
|
+
<meta property="og:type" content="website" />
|
|
320
278
|
|
|
321
|
-
|
|
279
|
+
<!-- Twitter Card -->
|
|
280
|
+
<meta name="twitter:card" content="summary" />
|
|
281
|
+
<meta name="twitter:title" content="${title}" />
|
|
282
|
+
<meta name="twitter:description" content="${blocks.descriptionTxt}" />
|
|
322
283
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
descriptionTxt: descriptionTxt,
|
|
326
|
-
gaHtml: pad(gaHtml, 2, 1),
|
|
327
|
-
jseeHtml: jseeHtml,
|
|
328
|
-
hiddenElementHtml: hiddenElementHtml,
|
|
329
|
-
socialHtml: pad(socialHtml, 2, 1),
|
|
330
|
-
orgHtml: pad(orgHtml, 2, 1),
|
|
331
|
-
})
|
|
284
|
+
<!-- Structured Data -->
|
|
285
|
+
<script type="application/ld+json">{"@context":"https://schema.org","@type":"WebSite","headline":"${title}","name":"${title}","url":"${url}", "description":"${blocks.descriptionTxt}"}</script>
|
|
332
286
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
return html
|
|
336
|
-
} else if (outputs) {
|
|
337
|
-
// Store the html in the output file
|
|
338
|
-
for (let o of outputs) {
|
|
339
|
-
if (o === 'stdout') {
|
|
340
|
-
console.log(html)
|
|
341
|
-
} else if (o.includes('.html')) {
|
|
342
|
-
fs.writeFileSync(path.join(cwd, o), html)
|
|
343
|
-
} else if (o.includes('.json')) {
|
|
344
|
-
fs.writeFileSync(path.join(cwd, o), JSON.stringify(schema, null, 2))
|
|
345
|
-
} else if (o.includes('.md')) {
|
|
346
|
-
fs.writeFileSync(path.join(cwd, o), genMarkdownFromSchema(schema))
|
|
347
|
-
} else {
|
|
348
|
-
console.error('Invalid output file:', o)
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
fs.writeFileSync(path.join(cwd, outputs[0]), html)
|
|
352
|
-
} else {
|
|
353
|
-
// Serve the html
|
|
354
|
-
const express = require('express')
|
|
355
|
-
const app = express()
|
|
356
|
-
app.use(express.json())
|
|
357
|
-
if (argv.execute) {
|
|
358
|
-
// Create post endpoint for executing the model
|
|
359
|
-
schema.model.forEach(m => {
|
|
360
|
-
app.post(m.url, (req, res) => {
|
|
361
|
-
console.log(`Executing model: ${m.name}`)
|
|
362
|
-
if (m.name in modelFuncs) {
|
|
363
|
-
const modelFunc = modelFuncs[m.name]
|
|
364
|
-
try {
|
|
365
|
-
const result = modelFunc(req.body)
|
|
366
|
-
res.json(result)
|
|
367
|
-
} catch (error) {
|
|
368
|
-
console.error('Error executing model:', error)
|
|
369
|
-
res.status(500).json({ error: error.message })
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
})
|
|
373
|
-
console.log('Model execution endpoints created:', m.url)
|
|
374
|
-
})
|
|
375
|
-
}
|
|
376
|
-
app.get('/', async (req, res) => {
|
|
377
|
-
console.log('Serving index.html')
|
|
378
|
-
res.send(await gen(process.argv, true))
|
|
379
|
-
})
|
|
380
|
-
app.get('/dist/jsee.runtime.js', (req, res) => {
|
|
381
|
-
const pathToJSEE = path.join(__dirname, '..', 'dist', 'jsee.runtime.js')
|
|
382
|
-
console.log(`Serving jsee.runtime.js from ${pathToJSEE}`)
|
|
383
|
-
res.sendFile(pathToJSEE)
|
|
384
|
-
})
|
|
385
|
-
app.use(express.static(cwd))
|
|
386
|
-
app.listen(argv.port, () => {
|
|
387
|
-
console.log(`JSEE app is running: http://localhost:${argv.port}`)
|
|
388
|
-
})
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function genSchema (jsdocData) {
|
|
393
|
-
let schema = {
|
|
394
|
-
model: [],
|
|
395
|
-
inputs: [],
|
|
396
|
-
outputs: [],
|
|
397
|
-
}
|
|
398
|
-
for (let d of jsdocData) {
|
|
399
|
-
const model = {
|
|
400
|
-
name: d.name ? d.name : d.meta.filename.split('.')[0],
|
|
401
|
-
description: d.description ? d.description : '',
|
|
402
|
-
type: d.kind,
|
|
403
|
-
container: 'args',
|
|
404
|
-
url: path.relative(process.cwd(), path.join(d.meta.path, d.meta.filename)),
|
|
405
|
-
worker: false
|
|
406
|
-
}
|
|
407
|
-
if (d.requires) {
|
|
408
|
-
model.imports = d.requires.map(r => r.replace('module:', ''))
|
|
409
|
-
}
|
|
410
|
-
if (d.params) {
|
|
411
|
-
// Check if all params have the same name before '.'
|
|
412
|
-
const names = new Set(d.params.map(p => p.name.split('.')[0]))
|
|
413
|
-
if ((d.params.length > 1) && (names.size === 1)) {
|
|
414
|
-
// Object
|
|
415
|
-
model.container = 'object'
|
|
416
|
-
d.params.slice(1).forEach(p => {
|
|
417
|
-
const inp = {
|
|
418
|
-
name: p.name.split('.')[1],
|
|
419
|
-
type: p.type.names[0],
|
|
420
|
-
description: p.description,
|
|
421
|
-
}
|
|
422
|
-
if (p.defaultvalue) {
|
|
423
|
-
inp.default = p.defaultvalue
|
|
424
|
-
}
|
|
425
|
-
schema.inputs.push(inp)
|
|
426
|
-
})
|
|
427
|
-
} else {
|
|
428
|
-
// Array
|
|
429
|
-
model.container = 'args'
|
|
430
|
-
d.params.forEach(p => {
|
|
431
|
-
const inp = {
|
|
432
|
-
name: p.name,
|
|
433
|
-
type: p.type.names[0],
|
|
434
|
-
description: p.description,
|
|
435
|
-
}
|
|
436
|
-
if (p.defaultvalue) {
|
|
437
|
-
inp.default = p.defaultvalue
|
|
438
|
-
}
|
|
439
|
-
schema.inputs.push(inp)
|
|
440
|
-
})
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
if (d.returns) {
|
|
444
|
-
d.returns.forEach(r => {
|
|
445
|
-
r.name = r.name ? r.name : r.description.split('-')[0].trim()
|
|
446
|
-
r.description = r.description.split('-').slice(1).join('-').trim()
|
|
447
|
-
})
|
|
448
|
-
const names = new Set(d.returns.map(r => r.name.split('.')[0]))
|
|
449
|
-
if ((d.returns.length > 1) && (names.size === 1)) {
|
|
450
|
-
// Object
|
|
451
|
-
d.returns.slice(1).forEach(p => {
|
|
452
|
-
const out = {
|
|
453
|
-
name: p.name.split('.')[1],
|
|
454
|
-
type: p.type.names[0],
|
|
455
|
-
description: p.description,
|
|
456
|
-
}
|
|
457
|
-
schema.outputs.push(out)
|
|
458
|
-
})
|
|
459
|
-
} else {
|
|
460
|
-
// Array
|
|
461
|
-
d.returns.forEach(p => {
|
|
462
|
-
const out = {
|
|
463
|
-
name: p.name,
|
|
464
|
-
type: p.type.names[0],
|
|
465
|
-
description: p.description,
|
|
466
|
-
}
|
|
467
|
-
schema.outputs.push(out)
|
|
468
|
-
})
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
if (d.customTags) {
|
|
472
|
-
d.customTags.forEach(t => {
|
|
473
|
-
if (t.tag === 'worker') {
|
|
474
|
-
model.worker = true
|
|
475
|
-
}
|
|
476
|
-
})
|
|
477
|
-
}
|
|
478
|
-
schema.model.push(model)
|
|
479
|
-
}
|
|
480
|
-
return schema
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
function genHtmlFromSchema(schema) {
|
|
484
|
-
let htmlDescription = '<br><div class="schema-description">';
|
|
485
|
-
|
|
486
|
-
// Process the model section
|
|
487
|
-
if (schema.model && schema.model.length > 0) {
|
|
488
|
-
schema.model.forEach(model => {
|
|
489
|
-
htmlDescription += `<h3><strong>${model.name}</strong></h3>`
|
|
490
|
-
if (model.description) {
|
|
491
|
-
htmlDescription += `<p>${model.description}</p>`
|
|
492
|
-
}
|
|
493
|
-
})
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// Process the inputs section
|
|
497
|
-
if (schema.inputs && schema.inputs.length > 0) {
|
|
498
|
-
htmlDescription += '<h4>Inputs</h4><ul>';
|
|
499
|
-
schema.inputs.forEach(input => {
|
|
500
|
-
htmlDescription += `<li><strong>${input.name}</strong> (${input.type})`
|
|
501
|
-
if (input.description) {
|
|
502
|
-
htmlDescription += ` - ${input.description}`
|
|
503
|
-
}
|
|
504
|
-
})
|
|
505
|
-
htmlDescription += '</ul>';
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// Process the outputs section
|
|
509
|
-
if (schema.outputs && schema.outputs.length > 0) {
|
|
510
|
-
htmlDescription += '<h4>Outputs</h4><ul>';
|
|
511
|
-
schema.outputs.forEach(output => {
|
|
512
|
-
htmlDescription += `<li><strong>${output.name}</strong> (${output.type})`
|
|
513
|
-
if (output.description) {
|
|
514
|
-
htmlDescription += ` - ${output.description}`
|
|
515
|
-
}
|
|
516
|
-
})
|
|
517
|
-
htmlDescription += '</ul>';
|
|
518
|
-
}
|
|
519
|
-
htmlDescription += '</div>';
|
|
520
|
-
return htmlDescription;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
function genMarkdownFromSchema(schema) {
|
|
524
|
-
let markdownDescription = '';
|
|
525
|
-
|
|
526
|
-
// Process the model section
|
|
527
|
-
if (schema.model && schema.model.length > 0) {
|
|
528
|
-
schema.model.forEach(model => {
|
|
529
|
-
markdownDescription += `### **${model.name}**\n`;
|
|
530
|
-
if (model.description) {
|
|
531
|
-
markdownDescription += `${model.description}\n\n`;
|
|
532
|
-
}
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// Process the inputs section
|
|
537
|
-
if (schema.inputs && schema.inputs.length > 0) {
|
|
538
|
-
markdownDescription += '#### Inputs\n';
|
|
539
|
-
schema.inputs.forEach(input => {
|
|
540
|
-
markdownDescription += `- **${input.name}** (${input.type})`;
|
|
541
|
-
if (input.description) {
|
|
542
|
-
markdownDescription += ` - ${input.description}`;
|
|
543
|
-
}
|
|
544
|
-
markdownDescription += '\n';
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// Process the outputs section
|
|
549
|
-
if (schema.outputs && schema.outputs.length > 0) {
|
|
550
|
-
markdownDescription += '#### Outputs\n';
|
|
551
|
-
schema.outputs.forEach(output => {
|
|
552
|
-
markdownDescription += `- **${output.name}** (${output.type})`;
|
|
553
|
-
if (output.description) {
|
|
554
|
-
markdownDescription += ` - ${output.description}`;
|
|
555
|
-
}
|
|
556
|
-
markdownDescription += '\n';
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
return markdownDescription;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
function template(schema, blocks) {
|
|
564
|
-
let title = 'jsee'
|
|
565
|
-
// let url = schema.page && schema.page.url ? schema.page.url : ''
|
|
566
|
-
let url = ('page' in schema && 'url' in schema.page) ? schema.page.url : ''
|
|
567
|
-
if (schema.title) {
|
|
568
|
-
title = schema.title
|
|
569
|
-
} else if (schema.page && schema.page.title) {
|
|
570
|
-
title = schema.page.title
|
|
571
|
-
} else if (schema.model) {
|
|
572
|
-
if (Array.isArray(schema.model)) {
|
|
573
|
-
title = schema.model[0].name
|
|
574
|
-
} else {
|
|
575
|
-
title = schema.model.name
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
return `<!DOCTYPE html>
|
|
580
|
-
|
|
581
|
-
<!-- Generated by JSEE (https://jsee.org) -->
|
|
582
|
-
<!-- Do not edit this file directly. Edit the source files and run jsee to generate this file. -->
|
|
583
|
-
<!-- License: MIT (https://opensource.org/licenses/MIT) -->
|
|
584
|
-
|
|
585
|
-
<html lang="en">
|
|
586
|
-
<head>
|
|
587
|
-
<meta charset="utf-8">
|
|
588
|
-
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
589
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
590
|
-
<title>${title}</title>
|
|
591
|
-
<meta name="description" content="${blocks.descriptionTxt}">
|
|
592
|
-
|
|
593
|
-
<!-- Open Graph -->
|
|
594
|
-
<meta property="og:title" content="${title}" />
|
|
595
|
-
<meta property="og:description" content="${blocks.descriptionTxt}" />
|
|
596
|
-
<meta property="og:locale" content="en_US" />
|
|
597
|
-
<meta property="og:url" content="${url}" />
|
|
598
|
-
<meta property="og:site_name" content="${title}" />
|
|
599
|
-
<meta property="og:type" content="website" />
|
|
600
|
-
|
|
601
|
-
<!-- Twitter Card -->
|
|
602
|
-
<meta name="twitter:card" content="summary" />
|
|
603
|
-
<meta name="twitter:title" content="${title}" />
|
|
604
|
-
<meta name="twitter:description" content="${blocks.descriptionTxt}" />
|
|
605
|
-
|
|
606
|
-
<!-- Structured Data -->
|
|
607
|
-
<script type="application/ld+json">{"@context":"https://schema.org","@type":"WebSite","headline":"${title}","name":"${title}","url":"${url}", "description":"${blocks.descriptionTxt}"}</script>
|
|
608
|
-
|
|
609
|
-
<!-- Canonical Link -->
|
|
610
|
-
<link rel="canonical" href="${url}" />
|
|
287
|
+
<!-- Canonical Link -->
|
|
288
|
+
<link rel="canonical" href="${url}" />
|
|
611
289
|
|
|
612
290
|
<!-- Favicon -->
|
|
613
291
|
<link href="data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAgAAAAAAAAAAAAAAAEAAAAAAAAAD9/f0AAAAAAPj4+AAMDAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEQAAAAAAABERAAAAAAAAEREAAAAAAAAREQABERESABERAAETMzAAEREAARAAAAAREQABEAAAABERAAEQARAAEREAARABEAAREQABEAAAABERAAEQAAAAEREAAREREAAREQABEREQABERAAAAAAAAEREAAAAAAAAREQAAAAAAABHAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAA" rel="icon" type="image/x-icon" />
|
|
@@ -640,7 +318,7 @@ function template(schema, blocks) {
|
|
|
640
318
|
pre { padding: 8px 12px; overflow-x: auto; }
|
|
641
319
|
pre > code { border: 0; padding-right: 0; padding-left: 0; }
|
|
642
320
|
.wrapper { max-width: calc(800px - (30px)); margin-right: auto; margin-left: auto; padding-right: 15px; padding-left: 15px; }
|
|
643
|
-
@media screen and (min-width: 800px) { .wrapper { max-width: calc(
|
|
321
|
+
@media screen and (min-width: 800px) { .wrapper { max-width: calc(1024px - (30px * 2)); padding-right: 30px; padding-left: 30px; } }
|
|
644
322
|
.wrapper:after { content: ""; display: table; clear: both; }
|
|
645
323
|
.orange { color: #f66a0a; }
|
|
646
324
|
.grey { color: #828282; }
|
|
@@ -783,4 +461,440 @@ function template(schema, blocks) {
|
|
|
783
461
|
</html>`
|
|
784
462
|
}
|
|
785
463
|
|
|
464
|
+
// Adding async here breaks express. TODO: investigate
|
|
465
|
+
async function gen (pargv, returnHtml=false) {
|
|
466
|
+
// Determine if JSEE CLI is imported or run directly
|
|
467
|
+
const imported = path.dirname(__dirname) !== path.dirname(require.main.path)
|
|
468
|
+
|
|
469
|
+
// First pass over CLI arguments
|
|
470
|
+
// JSEE-level args
|
|
471
|
+
const argvAlias = {
|
|
472
|
+
inputs: 'i',
|
|
473
|
+
outputs: 'o',
|
|
474
|
+
description: 'd',
|
|
475
|
+
port: 'p',
|
|
476
|
+
version: 'v',
|
|
477
|
+
fetch: 'f',
|
|
478
|
+
execute: 'e',
|
|
479
|
+
cdn: 'c',
|
|
480
|
+
}
|
|
481
|
+
const argvDefault = {
|
|
482
|
+
execute: false, // execute the model code on the server
|
|
483
|
+
fetch: false, // fetch the JSEE runtime from the CDN or local server
|
|
484
|
+
inputs: 'schema.json', // default input is schema.json in the current working directory
|
|
485
|
+
port: 3000, // default port for the server
|
|
486
|
+
version: 'latest', // default version of JSEE runtime to use
|
|
487
|
+
verbose: false, // verbose mode
|
|
488
|
+
cdn: false,
|
|
489
|
+
}
|
|
490
|
+
let argv = minimist(pargv, {
|
|
491
|
+
alias: argvAlias,
|
|
492
|
+
default: argvDefault,
|
|
493
|
+
})
|
|
494
|
+
// Set argv.inputs to the first non-option argument if it exists
|
|
495
|
+
if (!imported && argv._.length > 0 && !argv.inputs) {
|
|
496
|
+
argv.inputs = argv._[0]
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function log (...args) {
|
|
500
|
+
if (argv.verbose) {
|
|
501
|
+
console.log('[JSEE CLI]', ...args)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
log('Imported:', imported)
|
|
506
|
+
log('Current working directory:', process.cwd())
|
|
507
|
+
log('Script location:', __dirname)
|
|
508
|
+
log('Script file:', __filename)
|
|
509
|
+
log('Require location:', require.main.path)
|
|
510
|
+
log('Require file:', require.main.filename)
|
|
511
|
+
|
|
512
|
+
let cwd = process.cwd()
|
|
513
|
+
let inputs = argv.inputs
|
|
514
|
+
let outputs = argv.outputs
|
|
515
|
+
let description = argv.description
|
|
516
|
+
let schema
|
|
517
|
+
let schemaPath
|
|
518
|
+
let descriptionTxt = ''
|
|
519
|
+
let descriptionHtml = ''
|
|
520
|
+
let jsdocMarkdown = ''
|
|
521
|
+
let modelFuncs = {}
|
|
522
|
+
|
|
523
|
+
// Determine the inputs and outputs
|
|
524
|
+
// if inputs is a string with js file names, split it into an array
|
|
525
|
+
if (typeof inputs === 'string') {
|
|
526
|
+
if (inputs.includes('.js')) {
|
|
527
|
+
inputs = inputs.split(',')
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// if outputs is a string with js file names, split it into an array
|
|
532
|
+
if (typeof outputs === 'string') {
|
|
533
|
+
outputs = outputs.split(',')
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (inputs.length === 0) {
|
|
537
|
+
console.error('No inputs provided')
|
|
538
|
+
process.exit(1)
|
|
539
|
+
} else if ((inputs.length === 1) && (inputs[0].includes('.json'))) {
|
|
540
|
+
// Input is json schema
|
|
541
|
+
// Curren working directory if not provided
|
|
542
|
+
// schema = require(path.join(cwd, inputs[0]))
|
|
543
|
+
// switch to fs.readFileSync to reload the schema if it changes
|
|
544
|
+
schemaPath = inputs[0].startsWith('/') ? inputs[0] : path.join(cwd, inputs[0])
|
|
545
|
+
schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'))
|
|
546
|
+
} else {
|
|
547
|
+
// Array of js files
|
|
548
|
+
// Generate schema
|
|
549
|
+
let jsdocData = jsdoc2md.getTemplateDataSync({ files: inputs.map(f => path.join(cwd, f)) })
|
|
550
|
+
schema = genSchema(jsdocData)
|
|
551
|
+
// jsdocMarkdown = jsdoc2md.renderSync({
|
|
552
|
+
// data: jsdocData,
|
|
553
|
+
// 'param-list-format': 'list',
|
|
554
|
+
// })
|
|
555
|
+
}
|
|
556
|
+
log('Schema path:', schemaPath)
|
|
557
|
+
|
|
558
|
+
// Second pass over CLI arguments
|
|
559
|
+
// Iterate over schema inputs and update argv aliases
|
|
560
|
+
if (schema.inputs) {
|
|
561
|
+
schema.inputs.forEach((inp, inp_index) => {
|
|
562
|
+
if (inp.name) {
|
|
563
|
+
const inputName = sanitizeName(inp.name)
|
|
564
|
+
if (inp.alias) {
|
|
565
|
+
argvAlias[inputName] = inp.alias
|
|
566
|
+
}
|
|
567
|
+
// Use positional arguments as schema inputs defaults if JSEE CLI is imported
|
|
568
|
+
if (imported && argv._.length > inp_index) {
|
|
569
|
+
log('Using positional argument for input:', inputName, argv._[inp_index])
|
|
570
|
+
argvDefault[inputName] = argv._[inp_index]
|
|
571
|
+
}
|
|
572
|
+
// We don't need to duplicate defaults here, as we handle them on the frontend
|
|
573
|
+
// else if (inp.default) {
|
|
574
|
+
// argvDefault[inputName] = inp.default
|
|
575
|
+
// }
|
|
576
|
+
}
|
|
577
|
+
})
|
|
578
|
+
}
|
|
579
|
+
// Update argv with the new aliases and defaults
|
|
580
|
+
argv = minimist(pargv, {
|
|
581
|
+
alias: argvAlias,
|
|
582
|
+
default: argvDefault,
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
// Now deactivate the inputs present in argv
|
|
586
|
+
// If you set parameter on the command line, it should not be editable in the GUI
|
|
587
|
+
// E.g. file selected
|
|
588
|
+
const dataFromArgvWithoutFileLoading = getDataFromArgv(schema, argv, false)
|
|
589
|
+
log('Data from argv without file loading:', dataFromArgvWithoutFileLoading)
|
|
590
|
+
if (schema.inputs) {
|
|
591
|
+
schema.inputs.forEach(inp => {
|
|
592
|
+
// Here data contains unsanitized input names
|
|
593
|
+
if (inp.name in dataFromArgvWithoutFileLoading) {
|
|
594
|
+
inp.default = dataFromArgvWithoutFileLoading[inp.name]
|
|
595
|
+
inp.disabled = true // Deactivate the input if it's present in argv
|
|
596
|
+
}
|
|
597
|
+
})
|
|
598
|
+
}
|
|
599
|
+
log('Argv:', argv)
|
|
600
|
+
|
|
601
|
+
// Initially in argv.fetch branch
|
|
602
|
+
// Check if schema has model, convert to array if needed
|
|
603
|
+
if (!schema.model) {
|
|
604
|
+
// console.error('No model found in schema')
|
|
605
|
+
// process.exit(1)
|
|
606
|
+
// It's still valid schema, can be only render function or vis of inputs/outputs
|
|
607
|
+
schema.model = []
|
|
608
|
+
}
|
|
609
|
+
if (!Array.isArray(schema.model)) {
|
|
610
|
+
schema.model = [schema.model]
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Server-side execution
|
|
614
|
+
// If execute is true, we will prepare the model functions to run on the server side
|
|
615
|
+
// Schema model will be updated with the server url and POST method
|
|
616
|
+
if (argv.execute) {
|
|
617
|
+
await Promise.all(schema.model.map(async m => {
|
|
618
|
+
log('Preparing a model to run on the server side:', m.name, m.url)
|
|
619
|
+
const target = require(path.join(schemaPath ? path.dirname(schemaPath) : cwd, m.url))
|
|
620
|
+
modelFuncs[m.name] = await getModelFuncJS(m, target, {log})
|
|
621
|
+
m.type = 'post'
|
|
622
|
+
m.url = `/${m.name}`
|
|
623
|
+
m.worker = false
|
|
624
|
+
}))
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Switch to CDN for model files
|
|
628
|
+
if (argv.cdn) {
|
|
629
|
+
let cdn = ''
|
|
630
|
+
console.log(argv)
|
|
631
|
+
if (typeof argv.cdn === 'string') {
|
|
632
|
+
cdn = argv.cdn
|
|
633
|
+
} else if (typeof argv.cdn === 'boolean') {
|
|
634
|
+
// Check package.json in cwd
|
|
635
|
+
const packageJsonPath = path.join(cwd, 'package.json')
|
|
636
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
637
|
+
const target = require(packageJsonPath)
|
|
638
|
+
const packageName = target.name
|
|
639
|
+
cdn = `https://cdn.jsdelivr.net/npm/${packageName}@${target.version}/`
|
|
640
|
+
} else {
|
|
641
|
+
console.error(`No package.json found: ${packageJsonPath}`)
|
|
642
|
+
process.exit(1)
|
|
643
|
+
}
|
|
644
|
+
} else {
|
|
645
|
+
console.error('Invalid CDN argument. Use --cdn <url> or --cdn true to use package.json version.')
|
|
646
|
+
process.exit(1)
|
|
647
|
+
}
|
|
648
|
+
log('Using CDN for model files:', cdn)
|
|
649
|
+
schema.model.forEach(m => {
|
|
650
|
+
if (m.url) {
|
|
651
|
+
// If url is relative, make it absolute
|
|
652
|
+
if (!m.url.startsWith('http')) {
|
|
653
|
+
m.url = path.join(cdn, m.url)
|
|
654
|
+
log(`Updated ${m.name} model URL to: ${m.url}`)
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
})
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
log('Schema:', schema)
|
|
661
|
+
|
|
662
|
+
// Generate description block
|
|
663
|
+
if (description) {
|
|
664
|
+
const descriptionMd = fs.readFileSync(path.join(cwd, description), 'utf8')
|
|
665
|
+
descriptionHtml = converter.makeHtml(descriptionMd)
|
|
666
|
+
|
|
667
|
+
if (descriptionMd.includes('---')) {
|
|
668
|
+
descriptionTxt = descriptionMd
|
|
669
|
+
.split('---')[0]
|
|
670
|
+
.replace(/\n/g, ' ')
|
|
671
|
+
.replace(/\s+/g, ' ')
|
|
672
|
+
.replace(/#/g, '')
|
|
673
|
+
.replace(/\*/g, '')
|
|
674
|
+
.trim()
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
descriptionHtml += genHtmlFromSchema(schema)
|
|
678
|
+
|
|
679
|
+
// Generate jsee code
|
|
680
|
+
let jseeHtml = ''
|
|
681
|
+
let hiddenElementHtml = ''
|
|
682
|
+
if (argv.fetch) {
|
|
683
|
+
// Fetch jsee code from the CDN or local server
|
|
684
|
+
let jseeCode
|
|
685
|
+
if (argv.version === 'dev') {
|
|
686
|
+
jseeCode = fs.readFileSync(path.join(__dirname, '..', 'dist', 'jsee.js'), 'utf8')
|
|
687
|
+
} else if (argv.version === 'latest') {
|
|
688
|
+
jseeCode = fs.readFileSync(path.join(__dirname, '..', 'dist', 'jsee.runtime.js'), 'utf8')
|
|
689
|
+
} else {
|
|
690
|
+
// Pre-fetch the jsee runtime from the CDN https://cdn.jsdelivr.net/npm/@jseeio/jsee@${argv.version}/dist/jsee.runtime.js
|
|
691
|
+
jseeCode = await fetch(`https://cdn.jsdelivr.net/npm/@jseeio/jsee@${argv.version}/dist/jsee.runtime.js`)
|
|
692
|
+
jseeCode = await jseeCode.text()
|
|
693
|
+
}
|
|
694
|
+
jseeHtml = `<script>${jseeCode}</script>`
|
|
695
|
+
// Fetch model files and store them in hidden elements
|
|
696
|
+
hiddenElementHtml += '<div id="hidden-storage" style="display: none;">'
|
|
697
|
+
|
|
698
|
+
for (let m of schema.model) {
|
|
699
|
+
if (m.type === 'get' || m.type === 'post') {
|
|
700
|
+
continue
|
|
701
|
+
}
|
|
702
|
+
if (m.url) {
|
|
703
|
+
// Fetch model from the local file system (url)
|
|
704
|
+
// TODO: Can be a remote URL (e.g. CDN)
|
|
705
|
+
const modelCode = fs.readFileSync(path.join(cwd, m.url), 'utf8')
|
|
706
|
+
hiddenElementHtml += `<script type="text/plain" style="display: none;" data-src="${m.url}">${modelCode}</script>`
|
|
707
|
+
}
|
|
708
|
+
if (m.imports) {
|
|
709
|
+
for (let i of m.imports) {
|
|
710
|
+
const importUrl = i.includes('.js') ? i : `https://cdn.jsdelivr.net/npm/${i}`
|
|
711
|
+
|
|
712
|
+
// Create cache directory if it doesn't exist
|
|
713
|
+
const cacheDir = path.join(os.homedir(), '.cache', 'jsee')
|
|
714
|
+
fs.mkdirSync(cacheDir, { recursive: true })
|
|
715
|
+
|
|
716
|
+
// Create a hash of the importUrl
|
|
717
|
+
const hash = crypto.createHash('sha256').update(importUrl).digest('hex')
|
|
718
|
+
const cacheFilePath = path.join(cacheDir, `${hash}.js`)
|
|
719
|
+
|
|
720
|
+
let importCode
|
|
721
|
+
let useCache = false
|
|
722
|
+
|
|
723
|
+
// Check if cache file exists and is less than 1 day old
|
|
724
|
+
if (fs.existsSync(cacheFilePath)) {
|
|
725
|
+
const stats = fs.statSync(cacheFilePath)
|
|
726
|
+
const mtime = new Date(stats.mtime)
|
|
727
|
+
const now = new Date()
|
|
728
|
+
const ageInDays = (now - mtime) / (1000 * 60 * 60 * 24)
|
|
729
|
+
|
|
730
|
+
if (ageInDays < 1) {
|
|
731
|
+
log('Using cached import:', importUrl)
|
|
732
|
+
importCode = fs.readFileSync(cacheFilePath, 'utf8');
|
|
733
|
+
useCache = true;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (!useCache) {
|
|
738
|
+
const response = await fetch(importUrl);
|
|
739
|
+
if (!response.ok) {
|
|
740
|
+
console.error(`Failed to fetch ${importUrl}: ${response.statusText}`);
|
|
741
|
+
process.exit(1);
|
|
742
|
+
}
|
|
743
|
+
importCode = await response.text()
|
|
744
|
+
fs.writeFileSync(cacheFilePath, importCode, 'utf8')
|
|
745
|
+
log('Fetched and stored to cache:', importUrl)
|
|
746
|
+
}
|
|
747
|
+
hiddenElementHtml += `<script type="text/plain" style="display: none;" data-src="${importUrl}">${importCode}</script>`
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
hiddenElementHtml += '</div>'
|
|
752
|
+
} else {
|
|
753
|
+
jseeHtml = outputs
|
|
754
|
+
? `<script src="https://cdn.jsdelivr.net/npm/@jseeio/jsee@${argv.version}/dist/jsee.runtime.js"></script>`
|
|
755
|
+
: argv.version === 'dev'
|
|
756
|
+
? `<script src="http://localhost:${argv.port}/dist/jsee.js"></script>`
|
|
757
|
+
: `<script src="http://localhost:${argv.port}/dist/jsee.runtime.js"></script>`
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
let socialHtml = ''
|
|
761
|
+
let gaHtml = ''
|
|
762
|
+
let orgHtml = ''
|
|
763
|
+
|
|
764
|
+
if (schema.page) {
|
|
765
|
+
if (schema.page.title) {
|
|
766
|
+
title = schema.page.title
|
|
767
|
+
}
|
|
768
|
+
if (schema.page.ga) {
|
|
769
|
+
gaHtml = `
|
|
770
|
+
<script id="ga-src" async src="https://www.googletagmanager.com/gtag/js?id=${schema.page.ga}"></script>
|
|
771
|
+
<script id="ga-body">
|
|
772
|
+
window['ga-disable-${schema.page.ga}'] = window.doNotTrack === "1" || navigator.doNotTrack === "1" || navigator.doNotTrack === "yes" || navigator.msDoNotTrack === "1";
|
|
773
|
+
window.dataLayer = window.dataLayer || [];
|
|
774
|
+
function gtag(){dataLayer.push(arguments);}
|
|
775
|
+
gtag('js', new Date());
|
|
776
|
+
gtag('config', '${schema.page.ga}');
|
|
777
|
+
</script>
|
|
778
|
+
`
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Social media links
|
|
782
|
+
if (schema.page.social) {
|
|
783
|
+
// iterate over dict with k, v pairs
|
|
784
|
+
for (let [name, url] of Object.entries(schema.page.social)) {
|
|
785
|
+
switch (name) {
|
|
786
|
+
case 'twitter':
|
|
787
|
+
socialHtml += `<li><a rel="me" href="https://twitter.com/${url}">Twitter</a></li>`
|
|
788
|
+
break
|
|
789
|
+
case 'github':
|
|
790
|
+
socialHtml += `<li><a rel="me" href="https://github.com/${url}">GitHub</a></li>`
|
|
791
|
+
break
|
|
792
|
+
case 'facebook':
|
|
793
|
+
socialHtml += `<li><a rel="me" href="https://www.facebook.com/${url}">Facebook</a></li>`
|
|
794
|
+
break
|
|
795
|
+
case 'linkedin':
|
|
796
|
+
socialHtml += `<li><a rel="me" href="https://www.linkedin.com/company/${url}">LinkedIn</a></li>`
|
|
797
|
+
break
|
|
798
|
+
case 'instagram':
|
|
799
|
+
socialHtml += `<li><a rel="me" href="https://www.instagram.com/${url}">Instagram</a></li>`
|
|
800
|
+
break
|
|
801
|
+
case 'youtube':
|
|
802
|
+
socialHtml += `<li><a rel="me" href="https://www.youtube.com/${url}">YouTube</a></li>`
|
|
803
|
+
break
|
|
804
|
+
default:
|
|
805
|
+
socialHtml += `<li><a rel="me" href="${s.url}">${s.name}</a></li>`
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (schema.page.org) {
|
|
811
|
+
orgHtml = `<div class="footer-org"><h4 class="footer-heading"><a href="${schema.page.org.url}">${schema.page.org.name}</a></h4>`
|
|
812
|
+
if (schema.page.org.description) {
|
|
813
|
+
orgHtml += `<p>${schema.page.org.description}</p>`
|
|
814
|
+
}
|
|
815
|
+
orgHtml += '</div>'
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const html = template(schema, {
|
|
821
|
+
descriptionHtml: pad(descriptionHtml, 8, 1),
|
|
822
|
+
descriptionTxt: descriptionTxt,
|
|
823
|
+
gaHtml: pad(gaHtml, 2, 1),
|
|
824
|
+
jseeHtml: jseeHtml,
|
|
825
|
+
hiddenElementHtml: hiddenElementHtml,
|
|
826
|
+
socialHtml: pad(socialHtml, 2, 1),
|
|
827
|
+
orgHtml: pad(orgHtml, 2, 1),
|
|
828
|
+
})
|
|
829
|
+
|
|
830
|
+
if (returnHtml) {
|
|
831
|
+
// Return the html as a string
|
|
832
|
+
return html
|
|
833
|
+
} else if (outputs) {
|
|
834
|
+
// Store the html in the output file
|
|
835
|
+
for (let o of outputs) {
|
|
836
|
+
if (o === 'stdout') {
|
|
837
|
+
log(html)
|
|
838
|
+
} else if (o.includes('.html')) {
|
|
839
|
+
fs.writeFileSync(path.join(cwd, o), html)
|
|
840
|
+
} else if (o.includes('.json')) {
|
|
841
|
+
fs.writeFileSync(path.join(cwd, o), JSON.stringify(schema, null, 2))
|
|
842
|
+
} else if (o.includes('.md')) {
|
|
843
|
+
fs.writeFileSync(path.join(cwd, o), genMarkdownFromSchema(schema))
|
|
844
|
+
} else {
|
|
845
|
+
console.error('Invalid output file:', o)
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
fs.writeFileSync(path.join(cwd, outputs[0]), html)
|
|
849
|
+
} else {
|
|
850
|
+
// Serve the html
|
|
851
|
+
const express = require('express')
|
|
852
|
+
const app = express()
|
|
853
|
+
app.use(express.json())
|
|
854
|
+
if (argv.execute) {
|
|
855
|
+
// Create post endpoint for executing the model
|
|
856
|
+
schema.model.forEach(m => {
|
|
857
|
+
app.post(m.url, (req, res) => {
|
|
858
|
+
log(`Executing model: ${m.name}`)
|
|
859
|
+
if (m.name in modelFuncs) {
|
|
860
|
+
const modelFunc = modelFuncs[m.name]
|
|
861
|
+
try {
|
|
862
|
+
const dataFromArgv = getDataFromArgv(schema, argv)
|
|
863
|
+
const dataFromGUI = req.body
|
|
864
|
+
const data = { ...dataFromGUI, ...dataFromArgv }
|
|
865
|
+
log('Data for model execution:', data)
|
|
866
|
+
const result = modelFunc(data)
|
|
867
|
+
res.json(result)
|
|
868
|
+
log(`Model ${m.name} executed successfully: `, result)
|
|
869
|
+
} catch (error) {
|
|
870
|
+
console.error('Error executing model:', error)
|
|
871
|
+
res.status(500).json({ error: error.message })
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
})
|
|
875
|
+
log('Model execution endpoints created:', m.url)
|
|
876
|
+
})
|
|
877
|
+
}
|
|
878
|
+
app.get('/', async (req, res) => {
|
|
879
|
+
log('Serving index.html')
|
|
880
|
+
res.send(await gen(pargv, true))
|
|
881
|
+
})
|
|
882
|
+
// app.get('/dist/jsee.runtime.js', (req, res) => {
|
|
883
|
+
// // __dirname points to this file location (it's jsee/src/cli.js, likely in node_modules)
|
|
884
|
+
// // so we need to go up one level to get to the dist folder with jsee.runtime.js
|
|
885
|
+
// const pathToJSEE = path.join(__dirname, '..', 'dist', 'jsee.runtime.js')
|
|
886
|
+
// log(`Serving jsee.runtime.js from: ${pathToJSEE}`)
|
|
887
|
+
// res.sendFile(pathToJSEE)
|
|
888
|
+
// })
|
|
889
|
+
app.use('/dist', express.static(path.join(__dirname, '..', 'dist'))) // Serve static files from the dist folder
|
|
890
|
+
// app.use(express.static(cwd))
|
|
891
|
+
// app.use(express.static(require.main.path)) // Serve static files from the main module path
|
|
892
|
+
// app.use(express.static(path.join(require.main.path, '..'))) // Serve static files from the parent directory of the main module path
|
|
893
|
+
app.use(express.static(schemaPath ? path.dirname(schemaPath) : cwd)) // Serve static files from the schema path or current working directory
|
|
894
|
+
app.listen(argv.port, () => {
|
|
895
|
+
console.log(`JSEE app is running: http://localhost:${argv.port}`)
|
|
896
|
+
})
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
786
900
|
module.exports = gen
|