node-plantuml-2 1.0.0
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/LICENSE +21 -0
- package/README.md +1053 -0
- package/index.js +8 -0
- package/lib/node-plantuml-cmd.js +110 -0
- package/lib/node-plantuml.js +446 -0
- package/lib/plantuml-executor-wasm.js +295 -0
- package/lib/plantuml-executor.js +165 -0
- package/lib/plantuml-syntax-fixer.js +545 -0
- package/nail/plantumlnail.jar +0 -0
- package/package.json +66 -0
- package/resources/classic.puml +31 -0
- package/resources/monochrome.puml +1 -0
- package/scripts/download.js +95 -0
- package/scripts/get-vizjs.js +51 -0
- package/vendor/plantuml.jar +0 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PlantUML Wasm Executor
|
|
5
|
+
*
|
|
6
|
+
* This module provides Wasm-based execution of PlantUML using TeaVM/Bytecoder.
|
|
7
|
+
*
|
|
8
|
+
* Implementation Strategy:
|
|
9
|
+
* 1. Use TeaVM or Bytecoder to convert PlantUML JAR to WebAssembly
|
|
10
|
+
* 2. Run Wasm module in Node.js using WASI or WebAssembly API
|
|
11
|
+
* 3. Provide file system access through WASI or Node.js FS API
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
var fs = require('fs')
|
|
15
|
+
var path = require('path')
|
|
16
|
+
var stream = require('stream')
|
|
17
|
+
|
|
18
|
+
// WASI is available in Node.js 12+ as experimental, stable in 20+
|
|
19
|
+
var WASI
|
|
20
|
+
try {
|
|
21
|
+
// Try Node.js 20+ stable API
|
|
22
|
+
WASI = require('wasi').WASI
|
|
23
|
+
} catch (e) {
|
|
24
|
+
try {
|
|
25
|
+
// Try experimental API (Node.js 12-19)
|
|
26
|
+
WASI = require('wasi').WASI
|
|
27
|
+
} catch (e2) {
|
|
28
|
+
// WASI not available
|
|
29
|
+
WASI = null
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
var WASM_DIR = path.join(__dirname, '../vendor/wasm')
|
|
34
|
+
var PLANTUML_WASM = path.join(WASM_DIR, 'plantuml.wasm')
|
|
35
|
+
var wasmInstance = null
|
|
36
|
+
var wasmMemory = null
|
|
37
|
+
var wasi = null
|
|
38
|
+
var wasmReady = false
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Initialize Wasm module
|
|
42
|
+
* @param {Function} callback - Callback when ready
|
|
43
|
+
*/
|
|
44
|
+
function initWasm (callback) {
|
|
45
|
+
if (wasmReady && wasmInstance) {
|
|
46
|
+
if (typeof callback === 'function') {
|
|
47
|
+
callback(null)
|
|
48
|
+
}
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!fs.existsSync(PLANTUML_WASM)) {
|
|
53
|
+
var err = new Error('Wasm module not found: ' + PLANTUML_WASM + '\nPlease run: node scripts/build-plantuml-wasm.js')
|
|
54
|
+
if (typeof callback === 'function') {
|
|
55
|
+
callback(err)
|
|
56
|
+
} else {
|
|
57
|
+
throw err
|
|
58
|
+
}
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// Check Node.js version for WASI support (Node.js 12+)
|
|
64
|
+
var nodeVersion = process.version
|
|
65
|
+
var majorVersion = parseInt(nodeVersion.split('.')[0].substring(1))
|
|
66
|
+
if (majorVersion < 12) {
|
|
67
|
+
throw new Error('WASI requires Node.js 12+. Current version: ' + nodeVersion)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Initialize WASI
|
|
71
|
+
var cwd = process.cwd()
|
|
72
|
+
wasi = new WASI({
|
|
73
|
+
version: 'preview1',
|
|
74
|
+
env: process.env,
|
|
75
|
+
preopens: {
|
|
76
|
+
'/': cwd,
|
|
77
|
+
'/tmp': require('os').tmpdir()
|
|
78
|
+
},
|
|
79
|
+
args: []
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// Load Wasm module
|
|
83
|
+
var wasmBuffer = fs.readFileSync(PLANTUML_WASM)
|
|
84
|
+
|
|
85
|
+
// Create import object for WASI
|
|
86
|
+
var importObject = {
|
|
87
|
+
wasi_snapshot_preview1: wasi.wasiImport
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Instantiate Wasm module
|
|
91
|
+
/* global WebAssembly */
|
|
92
|
+
WebAssembly.instantiate(wasmBuffer, importObject)
|
|
93
|
+
.then(function (result) {
|
|
94
|
+
wasmInstance = result.instance
|
|
95
|
+
wasmMemory = wasmInstance.exports.memory
|
|
96
|
+
|
|
97
|
+
// Initialize WASI
|
|
98
|
+
wasi.initialize(wasmInstance)
|
|
99
|
+
|
|
100
|
+
wasmReady = true
|
|
101
|
+
console.log('✓ Wasm module loaded successfully')
|
|
102
|
+
|
|
103
|
+
if (typeof callback === 'function') {
|
|
104
|
+
callback(null)
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
.catch(function (err) {
|
|
108
|
+
console.error('Failed to load Wasm module:', err)
|
|
109
|
+
if (typeof callback === 'function') {
|
|
110
|
+
callback(err)
|
|
111
|
+
} else {
|
|
112
|
+
throw err
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (typeof callback === 'function') {
|
|
117
|
+
callback(err)
|
|
118
|
+
} else {
|
|
119
|
+
throw err
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Create a process-like object for Wasm execution
|
|
126
|
+
* @param {Array} argv - Command line arguments
|
|
127
|
+
* @param {string} cwd - Working directory
|
|
128
|
+
* @param {Function} callback - Callback function
|
|
129
|
+
* @returns {Object} Child process-like object with stdin/stdout/stderr
|
|
130
|
+
*/
|
|
131
|
+
function execWithWasm (argv, cwd, callback) {
|
|
132
|
+
if (!wasmReady || !wasmInstance) {
|
|
133
|
+
throw new Error('Wasm module not initialized. Call initWasm() first.')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Create streams for stdin/stdout/stderr
|
|
137
|
+
var stdinStream = new stream.PassThrough()
|
|
138
|
+
var stdoutStream = new stream.PassThrough()
|
|
139
|
+
var stderrStream = new stream.PassThrough()
|
|
140
|
+
|
|
141
|
+
// Collect stdin data
|
|
142
|
+
var stdinData = []
|
|
143
|
+
stdinStream.on('data', function (chunk) {
|
|
144
|
+
stdinData.push(chunk)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
stdinStream.on('end', function () {
|
|
148
|
+
// Process input when stdin ends
|
|
149
|
+
var inputBuffer = Buffer.concat(stdinData)
|
|
150
|
+
processWasmExecution(argv, inputBuffer, stdoutStream, stderrStream, cwd, callback)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// If no stdin data expected, process immediately
|
|
154
|
+
setTimeout(function () {
|
|
155
|
+
if (stdinData.length === 0 && stdinStream.readableEnded) {
|
|
156
|
+
processWasmExecution(argv, null, stdoutStream, stderrStream, cwd, callback)
|
|
157
|
+
}
|
|
158
|
+
}, 100)
|
|
159
|
+
|
|
160
|
+
// Return process-like object
|
|
161
|
+
return {
|
|
162
|
+
stdin: stdinStream,
|
|
163
|
+
stdout: stdoutStream,
|
|
164
|
+
stderr: stderrStream
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Process Wasm execution
|
|
170
|
+
* @private
|
|
171
|
+
*/
|
|
172
|
+
function processWasmExecution (argv, stdinData, stdoutStream, stderrStream, cwd, callback) {
|
|
173
|
+
try {
|
|
174
|
+
// Convert argv to string array for Wasm
|
|
175
|
+
var args = argv || []
|
|
176
|
+
var argsString = args.join(' ')
|
|
177
|
+
|
|
178
|
+
// Prepare input data
|
|
179
|
+
var inputText = stdinData ? stdinData.toString('utf-8') : ''
|
|
180
|
+
|
|
181
|
+
// Call Wasm main function
|
|
182
|
+
// Note: This is a simplified version. Actual PlantUML Wasm module
|
|
183
|
+
// may have different function signatures
|
|
184
|
+
if (wasmInstance.exports.main) {
|
|
185
|
+
// If main function exists, call it with arguments
|
|
186
|
+
var result = wasmInstance.exports.main(args.length, argsString, inputText)
|
|
187
|
+
|
|
188
|
+
// Read output from memory
|
|
189
|
+
if (wasmMemory && result !== undefined) {
|
|
190
|
+
// Parse result and write to stdout
|
|
191
|
+
// This is simplified - actual implementation depends on Wasm module API
|
|
192
|
+
stdoutStream.end(Buffer.from(result))
|
|
193
|
+
} else {
|
|
194
|
+
stdoutStream.end()
|
|
195
|
+
}
|
|
196
|
+
} else if (wasmInstance.exports._start) {
|
|
197
|
+
// WASI entry point
|
|
198
|
+
wasi.start(wasmInstance)
|
|
199
|
+
stdoutStream.end()
|
|
200
|
+
} else {
|
|
201
|
+
// Fallback: try to find PlantUML-specific export
|
|
202
|
+
console.warn('Wasm module does not export expected functions. PlantUML Wasm module may need custom integration.')
|
|
203
|
+
stdoutStream.end()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (typeof callback === 'function') {
|
|
207
|
+
var chunks = []
|
|
208
|
+
stdoutStream.on('data', function (chunk) {
|
|
209
|
+
chunks.push(chunk)
|
|
210
|
+
})
|
|
211
|
+
stdoutStream.on('end', function () {
|
|
212
|
+
var data = Buffer.concat(chunks)
|
|
213
|
+
callback(null, data)
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
} catch (err) {
|
|
217
|
+
stderrStream.write('Error executing Wasm module: ' + err.message + '\n')
|
|
218
|
+
stderrStream.end()
|
|
219
|
+
if (typeof callback === 'function') {
|
|
220
|
+
callback(err)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Check if Wasm executor is available
|
|
227
|
+
* @returns {boolean}
|
|
228
|
+
*/
|
|
229
|
+
function isWasmAvailable () {
|
|
230
|
+
return fs.existsSync(PLANTUML_WASM)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Check if Wasm executor is ready (initialized)
|
|
235
|
+
* @returns {boolean}
|
|
236
|
+
*/
|
|
237
|
+
function isReady () {
|
|
238
|
+
return wasmReady && wasmInstance !== null
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Initialize Wasm synchronously (for immediate use)
|
|
243
|
+
*/
|
|
244
|
+
function initWasmSync () {
|
|
245
|
+
if (wasmReady && wasmInstance) {
|
|
246
|
+
return true
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!fs.existsSync(PLANTUML_WASM)) {
|
|
250
|
+
return false
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
var nodeVersion = process.version
|
|
255
|
+
var majorVersion = parseInt(nodeVersion.split('.')[0].substring(1))
|
|
256
|
+
if (majorVersion < 12) {
|
|
257
|
+
return false
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
var cwd = process.cwd()
|
|
261
|
+
wasi = new WASI({
|
|
262
|
+
version: 'preview1',
|
|
263
|
+
env: process.env,
|
|
264
|
+
preopens: {
|
|
265
|
+
'/': cwd,
|
|
266
|
+
'/tmp': require('os').tmpdir()
|
|
267
|
+
},
|
|
268
|
+
args: []
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
var wasmBuffer = fs.readFileSync(PLANTUML_WASM)
|
|
272
|
+
var importObject = {
|
|
273
|
+
wasi_snapshot_preview1: wasi.wasiImport
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* global WebAssembly */
|
|
277
|
+
var result = WebAssembly.instantiateSync(wasmBuffer, importObject)
|
|
278
|
+
wasmInstance = result.instance
|
|
279
|
+
wasmMemory = wasmInstance.exports.memory
|
|
280
|
+
wasi.initialize(wasmInstance)
|
|
281
|
+
wasmReady = true
|
|
282
|
+
return true
|
|
283
|
+
} catch (e) {
|
|
284
|
+
console.warn('Failed to initialize Wasm synchronously:', e.message)
|
|
285
|
+
return false
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
module.exports = {
|
|
290
|
+
initWasm: initWasm,
|
|
291
|
+
exec: execWithWasm,
|
|
292
|
+
isAvailable: isWasmAvailable,
|
|
293
|
+
isReady: isReady,
|
|
294
|
+
initWasmSync: initWasmSync
|
|
295
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
var childProcess = require('child_process')
|
|
4
|
+
var path = require('path')
|
|
5
|
+
var nailgun = require('node-nailgun-server')
|
|
6
|
+
var ngClient = require('node-nailgun-client')
|
|
7
|
+
|
|
8
|
+
var INCLUDED_PLANTUML_JAR = path.join(__dirname, '../vendor/plantuml.jar')
|
|
9
|
+
var PLANTUML_JAR = process.env.PLANTUML_HOME || INCLUDED_PLANTUML_JAR
|
|
10
|
+
|
|
11
|
+
var PLANTUML_NAIL_JAR = path.join(__dirname, '../nail/plantumlnail.jar')
|
|
12
|
+
var PLANTUML_NAIL_CLASS = 'PlantumlNail'
|
|
13
|
+
|
|
14
|
+
var LOCALHOST = 'localhost'
|
|
15
|
+
var GENERATE_PORT = 0
|
|
16
|
+
|
|
17
|
+
var nailgunServer
|
|
18
|
+
var clientOptions
|
|
19
|
+
var nailgunRunning = false
|
|
20
|
+
|
|
21
|
+
module.exports.useNailgun = function (callback) {
|
|
22
|
+
var options = { address: LOCALHOST, port: GENERATE_PORT }
|
|
23
|
+
nailgunServer = nailgun.createServer(options, function (port) {
|
|
24
|
+
clientOptions = {
|
|
25
|
+
host: LOCALHOST,
|
|
26
|
+
port: port
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
ngClient.exec('ng-cp', [PLANTUML_JAR], clientOptions)
|
|
30
|
+
ngClient.exec('ng-cp', [PLANTUML_NAIL_JAR], clientOptions)
|
|
31
|
+
|
|
32
|
+
// Give Nailgun some time to load the classpath
|
|
33
|
+
setTimeout(function () {
|
|
34
|
+
nailgunRunning = true
|
|
35
|
+
if (typeof callback === 'function') {
|
|
36
|
+
callback()
|
|
37
|
+
}
|
|
38
|
+
}, 50)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
return nailgunServer
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// TODO: proper error handling
|
|
45
|
+
function execWithNailgun (argv, cwd, cb) {
|
|
46
|
+
clientOptions.cwd = cwd || process.cwd()
|
|
47
|
+
return ngClient.exec(PLANTUML_NAIL_CLASS, argv, clientOptions)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// TODO: proper error handling
|
|
51
|
+
function execWithSpawn (argv, cwd, cb) {
|
|
52
|
+
cwd = cwd || process.cwd()
|
|
53
|
+
var opts = [
|
|
54
|
+
'-Dplantuml.include.path=' + cwd,
|
|
55
|
+
'-Djava.awt.headless=true',
|
|
56
|
+
'-Dfile.encoding=UTF-8',
|
|
57
|
+
'-Duser.language=en',
|
|
58
|
+
'-Duser.country=US',
|
|
59
|
+
'-jar', PLANTUML_JAR
|
|
60
|
+
].concat(argv)
|
|
61
|
+
return childProcess.spawn('java', opts)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports.useWasm = function (callback) {
|
|
65
|
+
var wasmExecutor = require('./plantuml-executor-wasm')
|
|
66
|
+
if (wasmExecutor.isAvailable()) {
|
|
67
|
+
return wasmExecutor.initWasm(callback)
|
|
68
|
+
} else {
|
|
69
|
+
console.warn('Wasm executor not available, falling back to Java executor')
|
|
70
|
+
if (typeof callback === 'function') {
|
|
71
|
+
callback(new Error('Wasm executor not available'))
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports.exec = function (argv, cwd, callback) {
|
|
77
|
+
if (typeof argv === 'function') {
|
|
78
|
+
callback = argv
|
|
79
|
+
argv = undefined
|
|
80
|
+
cwd = undefined
|
|
81
|
+
} else if (typeof cwd === 'function') {
|
|
82
|
+
callback = cwd
|
|
83
|
+
cwd = undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Priority 1: Try Wasm executor first (pure Node, no Java needed)
|
|
87
|
+
var wasmExecutor = require('./plantuml-executor-wasm')
|
|
88
|
+
var useJava = process.env.PLANTUML_USE_JAVA === 'true' || process.env.PLANTUML_USE_JAVA === '1'
|
|
89
|
+
|
|
90
|
+
var task
|
|
91
|
+
// Use Wasm by default, unless explicitly requested to use Java
|
|
92
|
+
if (!useJava && wasmExecutor.isAvailable()) {
|
|
93
|
+
try {
|
|
94
|
+
// Try to initialize Wasm synchronously first
|
|
95
|
+
if (!wasmExecutor.isReady()) {
|
|
96
|
+
if (!wasmExecutor.initWasmSync()) {
|
|
97
|
+
// Sync init failed, try async (non-blocking)
|
|
98
|
+
wasmExecutor.initWasm(function (err) {
|
|
99
|
+
if (err) {
|
|
100
|
+
console.warn('Wasm initialization failed, falling back to Java:', err.message)
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Try to use Wasm executor
|
|
107
|
+
if (wasmExecutor.isReady()) {
|
|
108
|
+
task = wasmExecutor.exec(argv, cwd, callback)
|
|
109
|
+
// Wasm executor handles its own callback setup
|
|
110
|
+
if (task && typeof callback === 'function') {
|
|
111
|
+
// Setup callback for Wasm executor if needed
|
|
112
|
+
var chunks = []
|
|
113
|
+
if (task.stdout) {
|
|
114
|
+
task.stdout.on('data', function (chunk) { chunks.push(chunk) })
|
|
115
|
+
task.stdout.on('end', function () {
|
|
116
|
+
var data = Buffer.concat(chunks)
|
|
117
|
+
callback(null, data)
|
|
118
|
+
})
|
|
119
|
+
task.stdout.on('error', function () {
|
|
120
|
+
callback(new Error('error while reading plantuml output'), null)
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return task
|
|
125
|
+
} else {
|
|
126
|
+
// Wasm not ready yet, fallback to Java
|
|
127
|
+
task = getJavaTask(argv, cwd, callback)
|
|
128
|
+
}
|
|
129
|
+
} catch (e) {
|
|
130
|
+
console.warn('Wasm executor failed, falling back to Java:', e.message)
|
|
131
|
+
task = getJavaTask(argv, cwd, callback)
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
// Use Java executor (fallback or explicitly requested)
|
|
135
|
+
task = getJavaTask(argv, cwd, callback)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return task
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get Java executor task
|
|
143
|
+
*/
|
|
144
|
+
function getJavaTask (argv, cwd, callback) {
|
|
145
|
+
var task
|
|
146
|
+
if (nailgunRunning) {
|
|
147
|
+
task = execWithNailgun(argv, cwd, callback)
|
|
148
|
+
} else {
|
|
149
|
+
task = execWithSpawn(argv, cwd, callback)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (typeof callback === 'function') {
|
|
153
|
+
var chunks = []
|
|
154
|
+
task.stdout.on('data', function (chunk) { chunks.push(chunk) })
|
|
155
|
+
task.stdout.on('end', function () {
|
|
156
|
+
var data = Buffer.concat(chunks)
|
|
157
|
+
callback(null, data)
|
|
158
|
+
})
|
|
159
|
+
task.stdout.on('error', function () {
|
|
160
|
+
callback(new Error('error while reading plantuml output'), null)
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return task
|
|
165
|
+
}
|