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.
@@ -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
+ }