@vdwpsmt/node-red-contrib-flow-splitter-extended 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -8
- package/functions-templates-handler.js +11 -72
- package/index.js +197 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,13 +22,18 @@ It will make the diffs of your version control much more controlled and readable
|
|
|
22
22
|
- Supports both YAML and JSON formats
|
|
23
23
|
- Maintains tab order through configuration
|
|
24
24
|
|
|
25
|
-
### Function & Template Extraction (NEW)
|
|
26
|
-
- Automatically extracts
|
|
27
|
-
- Extracts `
|
|
28
|
-
-
|
|
29
|
-
-
|
|
25
|
+
### Optional Function & Template Extraction and Restore (NEW)
|
|
26
|
+
- Automatically extracts into seperate files per function/ui-template
|
|
27
|
+
- Extracts code from `function` nodes into `.js` files
|
|
28
|
+
- Extracts `ui-template` (Dashboard 2.0) content into `.vue` files
|
|
29
|
+
- Supports function `initialize` and `finalize` code in separate files
|
|
30
|
+
- Extracts node `info` documentation into `.md` files
|
|
30
31
|
- Organizes extracted files in subdirectories alongside their parent tab/subflow
|
|
31
|
-
-
|
|
32
|
+
- Restores changes back to Node-RED
|
|
33
|
+
- Automatically on startup
|
|
34
|
+
- Manually reload using endpoint
|
|
35
|
+
- Both extraction (default true) and restoring (default false) can be changed in `.config.flow-splitter.json`
|
|
36
|
+
|
|
32
37
|
|
|
33
38
|
## Functioning
|
|
34
39
|
|
|
@@ -81,7 +86,7 @@ fetch('http://localhost:1880/flow-splitter/reload', {method: 'POST'})
|
|
|
81
86
|
```
|
|
82
87
|
|
|
83
88
|
This allows you to:
|
|
84
|
-
1. Edit function/template files in VS Code
|
|
89
|
+
1. Edit function/template files in VS Code or any other IDE
|
|
85
90
|
2. Save changes
|
|
86
91
|
3. Run the reload command
|
|
87
92
|
4. See changes immediately in Node-RED (without deploy/restart)
|
|
@@ -124,7 +129,9 @@ Default configuration file =
|
|
|
124
129
|
"fileFormat": "yaml",
|
|
125
130
|
"destinationFolder": "src",
|
|
126
131
|
"tabsOrder": [],
|
|
127
|
-
"extractFunctionsTemplates": true
|
|
132
|
+
"extractFunctionsTemplates": true,
|
|
133
|
+
"restoreFunctionsTemplates": false,
|
|
134
|
+
"enableArtifact": false
|
|
128
135
|
}
|
|
129
136
|
```
|
|
130
137
|
|
|
@@ -134,6 +141,7 @@ You can freely edit the config file, the changes are taken into account at the n
|
|
|
134
141
|
- `destinationFolder`: path where to create the `tabs`, `subflows` and `config-nodes` sub-directories
|
|
135
142
|
- `tabsOrder`: position of each tab (ordered array of the Ids of each tab node)
|
|
136
143
|
- `extractFunctionsTemplates`: additional extraction of function and ui-template nodes
|
|
144
|
+
- `enableArtifact`: when `true`, writes a deployable monolith artifact to `artifact/flows.json` on reload/start events
|
|
137
145
|
## Installation
|
|
138
146
|
|
|
139
147
|
```bash
|
|
@@ -4,7 +4,7 @@ const fs = require('fs-extra')
|
|
|
4
4
|
/**
|
|
5
5
|
* Functions and Templates nodes Handler
|
|
6
6
|
* Extracts function and ui-template node code into separate files
|
|
7
|
-
* and
|
|
7
|
+
* and restores them back when rebuilding flows
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -18,6 +18,12 @@ function extractFunctionsAndTemplates(flowNodes, flowName, flowDir, RED) {
|
|
|
18
18
|
if (!flowNodes || flowNodes.length === 0) return
|
|
19
19
|
|
|
20
20
|
const extractedDir = path.join(flowDir, flowName)
|
|
21
|
+
|
|
22
|
+
// Delete entire extracted directory to ensure fresh state
|
|
23
|
+
if (fs.existsSync(extractedDir)) {
|
|
24
|
+
fs.removeSync(extractedDir)
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
const manifest = {}
|
|
22
28
|
const fileNames = []
|
|
23
29
|
let count = 0
|
|
@@ -124,20 +130,17 @@ function extractFunctionsAndTemplates(flowNodes, flowName, flowDir, RED) {
|
|
|
124
130
|
|
|
125
131
|
RED.log.info(`[node-red-contrib-flow-splitter] Extracted ${count} functions/templates for "${flowName}"`)
|
|
126
132
|
}
|
|
127
|
-
|
|
128
|
-
// Clean up unused files
|
|
129
|
-
cleanupUnusedFiles(extractedDir, manifest, RED)
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
/**
|
|
133
|
-
*
|
|
136
|
+
* Restore functions and templates from separate files back into flow nodes
|
|
134
137
|
* @param {Array} flowNodes - Array of nodes from a tab or subflow
|
|
135
138
|
* @param {string} flowName - Name of the tab or subflow
|
|
136
139
|
* @param {string} flowDir - Directory where the flow file is stored
|
|
137
140
|
* @param {object} RED - Node-RED runtime
|
|
138
141
|
* @returns {Array} - Updated flow nodes
|
|
139
142
|
*/
|
|
140
|
-
function
|
|
143
|
+
function restoreFunctionsAndTemplates(flowNodes, flowName, flowDir, RED) {
|
|
141
144
|
if (!flowNodes || flowNodes.length === 0) return flowNodes
|
|
142
145
|
|
|
143
146
|
const extractedDir = path.join(flowDir, flowName)
|
|
@@ -225,77 +228,13 @@ function collectFunctionsAndTemplates(flowNodes, flowName, flowDir, RED) {
|
|
|
225
228
|
})
|
|
226
229
|
|
|
227
230
|
if (updatedCount > 0) {
|
|
228
|
-
RED.log.info(`[node-red-contrib-flow-splitter]
|
|
231
|
+
RED.log.info(`[node-red-contrib-flow-splitter] Restored ${updatedCount} functions/templates for "${flowName}"`)
|
|
229
232
|
}
|
|
230
233
|
|
|
231
234
|
return flowNodes
|
|
232
235
|
}
|
|
233
236
|
|
|
234
|
-
/**
|
|
235
|
-
* Clean up files that are no longer in the manifest
|
|
236
|
-
* @param {string} extractedDir - Directory containing extracted files
|
|
237
|
-
* @param {object} manifest - Manifest object
|
|
238
|
-
* @param {object} RED - Node-RED runtime
|
|
239
|
-
*/
|
|
240
|
-
function cleanupUnusedFiles(extractedDir, manifest, RED) {
|
|
241
|
-
if (!fs.existsSync(extractedDir)) return
|
|
242
|
-
|
|
243
|
-
const validExtensions = ['.vue', '.js', '.md']
|
|
244
|
-
const files = getAllFiles(extractedDir, validExtensions)
|
|
245
|
-
|
|
246
|
-
files.forEach(file => {
|
|
247
|
-
// Skip manifest file
|
|
248
|
-
if (file.endsWith('.manifest.json')) return
|
|
249
|
-
|
|
250
|
-
let found = false
|
|
251
|
-
Object.keys(manifest).forEach((id) => {
|
|
252
|
-
const item = manifest[id]
|
|
253
|
-
if (file.indexOf(item.fileName) > -1) {
|
|
254
|
-
found = true
|
|
255
|
-
}
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
if (!found) {
|
|
259
|
-
const filePath = path.join(extractedDir, file)
|
|
260
|
-
try {
|
|
261
|
-
fs.removeSync(filePath)
|
|
262
|
-
RED.log.info(`[node-red-contrib-flow-splitter] Removed unused file: ${file}`)
|
|
263
|
-
} catch (error) {
|
|
264
|
-
RED.log.warn(`[node-red-contrib-flow-splitter] Could not remove file ${file}: ${error.message}`)
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
})
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Get all files with specified extensions from a directory
|
|
272
|
-
* @param {string} dir - Directory to search
|
|
273
|
-
* @param {Array<string>} exts - Array of extensions to match
|
|
274
|
-
* @param {Array<string>} fileList - Accumulated file list
|
|
275
|
-
* @param {string} relDir - Relative directory path
|
|
276
|
-
* @returns {Array<string>} - List of file paths
|
|
277
|
-
*/
|
|
278
|
-
function getAllFiles(dir, exts, fileList = [], relDir = '') {
|
|
279
|
-
if (!fs.existsSync(dir)) return fileList
|
|
280
|
-
|
|
281
|
-
const files = fs.readdirSync(dir)
|
|
282
|
-
|
|
283
|
-
files.forEach(file => {
|
|
284
|
-
const filePath = path.join(dir, file)
|
|
285
|
-
const relPath = path.join(relDir, file)
|
|
286
|
-
const stat = fs.statSync(filePath)
|
|
287
|
-
|
|
288
|
-
if (stat.isDirectory()) {
|
|
289
|
-
getAllFiles(filePath, exts, fileList, relPath)
|
|
290
|
-
} else if (exts.some(ext => file.endsWith(ext))) {
|
|
291
|
-
fileList.push(relPath)
|
|
292
|
-
}
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
return fileList
|
|
296
|
-
}
|
|
297
|
-
|
|
298
237
|
module.exports = {
|
|
299
238
|
extractFunctionsAndTemplates,
|
|
300
|
-
|
|
239
|
+
restoreFunctionsAndTemplates
|
|
301
240
|
}
|
package/index.js
CHANGED
|
@@ -25,12 +25,16 @@ const functionsTemplatesHandler = require('./functions-templates-handler')
|
|
|
25
25
|
let RED
|
|
26
26
|
|
|
27
27
|
const splitCfgFilename = '.config.flow-splitter.json'
|
|
28
|
+
const artifactsDirname = 'artifact'
|
|
29
|
+
const artifactsFilename = 'flows.json'
|
|
28
30
|
const DEFAULT_CFG = {
|
|
29
31
|
fileFormat: 'yaml',
|
|
30
32
|
destinationFolder: 'src',
|
|
31
33
|
tabsOrder: [],
|
|
32
34
|
monolithFilename: "flows.json",
|
|
33
|
-
extractFunctionsTemplates: true
|
|
35
|
+
extractFunctionsTemplates: true,
|
|
36
|
+
restoreFunctionsTemplates: false,
|
|
37
|
+
enableArtifact: false
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
/**
|
|
@@ -113,6 +117,142 @@ function extractFunctionsTemplatesFromSplitFiles(cfg, projectPath) {
|
|
|
113
117
|
|
|
114
118
|
processFlowDirectory(tabsDir, cfg.fileFormat, 'tab')
|
|
115
119
|
processFlowDirectory(subflowsDir, cfg.fileFormat, 'subflow')
|
|
120
|
+
|
|
121
|
+
// Clean up orphaned directories from renamed/deleted flows
|
|
122
|
+
cleanupOrphanedDirectories(tabsDir, cfg.fileFormat)
|
|
123
|
+
cleanupOrphanedDirectories(subflowsDir, cfg.fileFormat)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Remove subdirectories that don't have a corresponding flow file
|
|
128
|
+
* @param {string} dir - Directory to clean (tabs or subflows)
|
|
129
|
+
* @param {string} fileFormat - File format (yaml or json)
|
|
130
|
+
*/
|
|
131
|
+
function cleanupOrphanedDirectories(dir, fileFormat) {
|
|
132
|
+
if (!fs.existsSync(dir)) {
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const extension = fileFormat === 'yaml' ? '.yaml' : '.json'
|
|
137
|
+
|
|
138
|
+
// Get all flow files
|
|
139
|
+
const flowFiles = fs.readdirSync(dir)
|
|
140
|
+
.filter(f => f.endsWith(extension))
|
|
141
|
+
.map(f => path.basename(f, extension))
|
|
142
|
+
|
|
143
|
+
// Get all subdirectories
|
|
144
|
+
const subdirs = fs.readdirSync(dir)
|
|
145
|
+
.filter(f => {
|
|
146
|
+
const fullPath = path.join(dir, f)
|
|
147
|
+
return fs.statSync(fullPath).isDirectory()
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
// Remove orphaned subdirectories
|
|
151
|
+
subdirs.forEach(subdir => {
|
|
152
|
+
if (!flowFiles.includes(subdir)) {
|
|
153
|
+
const subdirPath = path.join(dir, subdir)
|
|
154
|
+
try {
|
|
155
|
+
fs.rmSync(subdirPath, { recursive: true, force: true })
|
|
156
|
+
RED.log.info(`[node-red-contrib-flow-splitter-extended] Removed orphaned directory: ${subdir}`)
|
|
157
|
+
} catch (error) {
|
|
158
|
+
RED.log.warn(`[node-red-contrib-flow-splitter-extended] Could not remove orphaned directory ${subdir}: ${error.message}`)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Clean up old flow files when a tab or subflow has been renamed.
|
|
166
|
+
* Scans existing files and removes those with IDs that match current flows but have different filenames.
|
|
167
|
+
* @param {Array} flowNodes - Array of all flow nodes from Node-RED
|
|
168
|
+
* @param {object} cfg - Splitter configuration
|
|
169
|
+
* @param {string} projectPath - Path to the project
|
|
170
|
+
*/
|
|
171
|
+
function cleanupRenamedFlows(flowNodes, cfg, projectPath) {
|
|
172
|
+
const srcDir = path.join(projectPath, cfg.destinationFolder || 'src')
|
|
173
|
+
const tabsDir = path.join(srcDir, 'tabs')
|
|
174
|
+
const subflowsDir = path.join(srcDir, 'subflows')
|
|
175
|
+
const extension = cfg.fileFormat === 'yaml' ? '.yaml' : '.json'
|
|
176
|
+
|
|
177
|
+
// Build maps of ID -> expected filename from the current flow nodes
|
|
178
|
+
const tabsIdToFilename = new Map()
|
|
179
|
+
const subflowsIdToFilename = new Map()
|
|
180
|
+
|
|
181
|
+
flowNodes.forEach(node => {
|
|
182
|
+
if (node.type === 'tab' && node.id) {
|
|
183
|
+
// Use normalizedLabel if available, otherwise compute from label
|
|
184
|
+
const label = node.label || node.id
|
|
185
|
+
const expectedFilename = node.normalizedLabel ||
|
|
186
|
+
label.replace(/[\/\\:*?"<>|]/g, '-').toLowerCase().replace(/\s+/g, '-')
|
|
187
|
+
tabsIdToFilename.set(node.id, expectedFilename)
|
|
188
|
+
} else if (node.type === 'subflow' && node.id) {
|
|
189
|
+
const name = node.name || node.id
|
|
190
|
+
const expectedFilename = name.replace(/[\/\\:*?"<>|]/g, '-').toLowerCase().replace(/\s+/g, '-')
|
|
191
|
+
subflowsIdToFilename.set(node.id, expectedFilename)
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
// Clean up tabs directory
|
|
196
|
+
cleanupRenamedFlowsInDir(tabsDir, tabsIdToFilename, extension, 'tab')
|
|
197
|
+
|
|
198
|
+
// Clean up subflows directory
|
|
199
|
+
cleanupRenamedFlowsInDir(subflowsDir, subflowsIdToFilename, extension, 'subflow')
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Clean up renamed flows in a specific directory.
|
|
204
|
+
* Removes old files when the same ID exists but with a different filename.
|
|
205
|
+
* @param {string} dir - Directory to scan
|
|
206
|
+
* @param {Map} idToFilename - Map of ID to expected filename
|
|
207
|
+
* @param {string} extension - File extension (.yaml or .json)
|
|
208
|
+
* @param {string} flowType - Type of flow (tab or subflow)
|
|
209
|
+
*/
|
|
210
|
+
function cleanupRenamedFlowsInDir(dir, idToFilename, extension, flowType) {
|
|
211
|
+
if (!fs.existsSync(dir)) {
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith(extension))
|
|
216
|
+
|
|
217
|
+
files.forEach(file => {
|
|
218
|
+
const filePath = path.join(dir, file)
|
|
219
|
+
const filename = path.basename(file, extension)
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
let flowData
|
|
223
|
+
const fileContent = fs.readFileSync(filePath, 'utf8')
|
|
224
|
+
|
|
225
|
+
if (extension === '.yaml') {
|
|
226
|
+
flowData = yaml.load(fileContent)
|
|
227
|
+
} else {
|
|
228
|
+
flowData = JSON.parse(fileContent)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const flowDataArray = Array.isArray(flowData) ? flowData : [flowData]
|
|
232
|
+
const flowNode = flowDataArray.find(n => n.type === flowType)
|
|
233
|
+
|
|
234
|
+
if (flowNode && flowNode.id) {
|
|
235
|
+
const expectedFilename = idToFilename.get(flowNode.id)
|
|
236
|
+
|
|
237
|
+
// If this ID exists in current flows but with a different filename, this is an old renamed file
|
|
238
|
+
if (expectedFilename && expectedFilename !== filename) {
|
|
239
|
+
RED.log.info(`[node-red-contrib-flow-splitter-extended] Removing old ${flowType} file "${file}" (renamed to "${expectedFilename}${extension}")`)
|
|
240
|
+
|
|
241
|
+
// Remove the old flow file
|
|
242
|
+
fs.unlinkSync(filePath)
|
|
243
|
+
|
|
244
|
+
// Remove the corresponding subdirectory if it exists
|
|
245
|
+
const subdirPath = path.join(dir, filename)
|
|
246
|
+
if (fs.existsSync(subdirPath) && fs.statSync(subdirPath).isDirectory()) {
|
|
247
|
+
fs.rmSync(subdirPath, { recursive: true, force: true })
|
|
248
|
+
RED.log.info(`[node-red-contrib-flow-splitter-extended] Removed old ${flowType} directory "${filename}"`)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
} catch (error) {
|
|
253
|
+
RED.log.warn(`[node-red-contrib-flow-splitter-extended] Error checking ${flowType} file ${file}: ${error.message}`)
|
|
254
|
+
}
|
|
255
|
+
})
|
|
116
256
|
}
|
|
117
257
|
|
|
118
258
|
/**
|
|
@@ -153,12 +293,12 @@ function processFlowDirectory(dir, fileFormat, flowType) {
|
|
|
153
293
|
}
|
|
154
294
|
|
|
155
295
|
/**
|
|
156
|
-
*
|
|
296
|
+
* Restore functions and templates back into split flow files before rebuilding single flows.json file
|
|
157
297
|
* @param {object} cfg - Splitter configuration
|
|
158
298
|
* @param {string} projectPath - Path to the project
|
|
159
299
|
*/
|
|
160
|
-
function
|
|
161
|
-
if (cfg.
|
|
300
|
+
function restoreFunctionsTemplatesIntoSplitFiles(cfg, projectPath) {
|
|
301
|
+
if (cfg.restoreFunctionsTemplates === false) {
|
|
162
302
|
return
|
|
163
303
|
}
|
|
164
304
|
|
|
@@ -166,19 +306,19 @@ function collectFunctionsTemplatesIntoSplitFiles(cfg, projectPath) {
|
|
|
166
306
|
const tabsDir = path.join(srcDir, 'tabs')
|
|
167
307
|
const subflowsDir = path.join(srcDir, 'subflows')
|
|
168
308
|
|
|
169
|
-
RED.log.info("[node-red-contrib-flow-splitter-extended]
|
|
309
|
+
RED.log.info("[node-red-contrib-flow-splitter-extended] Restoring functions and templates...")
|
|
170
310
|
|
|
171
|
-
|
|
172
|
-
|
|
311
|
+
restoreIntoFlowDirectory(tabsDir, cfg.fileFormat, 'tab')
|
|
312
|
+
restoreIntoFlowDirectory(subflowsDir, cfg.fileFormat, 'subflow')
|
|
173
313
|
}
|
|
174
314
|
|
|
175
315
|
/**
|
|
176
|
-
* Process a directory of flow files to
|
|
316
|
+
* Process a directory of flow files to restore functions/templates
|
|
177
317
|
* @param {string} dir - Directory to process
|
|
178
318
|
* @param {string} fileFormat - File format (yaml or json)
|
|
179
319
|
* @param {string} flowType - Type of flow (tab or subflow)
|
|
180
320
|
*/
|
|
181
|
-
function
|
|
321
|
+
function restoreIntoFlowDirectory(dir, fileFormat, flowType) {
|
|
182
322
|
if (!fs.existsSync(dir)) {
|
|
183
323
|
return
|
|
184
324
|
}
|
|
@@ -201,7 +341,7 @@ function collectFromFlowDirectory(dir, fileFormat, flowType) {
|
|
|
201
341
|
}
|
|
202
342
|
|
|
203
343
|
let flowNodes = Array.isArray(flowData) ? flowData : [flowData]
|
|
204
|
-
flowNodes = functionsTemplatesHandler.
|
|
344
|
+
flowNodes = functionsTemplatesHandler.restoreFunctionsAndTemplates(flowNodes, flowName, dir, RED)
|
|
205
345
|
|
|
206
346
|
if (fileFormat === 'yaml') {
|
|
207
347
|
const yamlContent = yaml.dump(flowNodes, {
|
|
@@ -216,14 +356,50 @@ function collectFromFlowDirectory(dir, fileFormat, flowType) {
|
|
|
216
356
|
}
|
|
217
357
|
|
|
218
358
|
} catch (error) {
|
|
219
|
-
RED.log.warn(`[node-red-contrib-flow-splitter-extended] Error
|
|
359
|
+
RED.log.warn(`[node-red-contrib-flow-splitter-extended] Error restoring ${flowType} ${flowName}: ${error.message}`)
|
|
220
360
|
}
|
|
221
361
|
})
|
|
222
362
|
}
|
|
223
363
|
|
|
364
|
+
/**
|
|
365
|
+
* Write a monolith artifact file to artifact/flows.json when enabled.
|
|
366
|
+
* @param {object} cfg - Splitter configuration
|
|
367
|
+
* @param {string} projectPath - Path to the project
|
|
368
|
+
* @param {Array} [flowNodes] - Optional flow nodes to serialize directly
|
|
369
|
+
*/
|
|
370
|
+
function writeMonolithArtifact(cfg, projectPath, flowNodes) {
|
|
371
|
+
if (cfg.enableArtifact !== true) {
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const artifactDir = path.join(projectPath, artifactsDirname)
|
|
376
|
+
const artifactPath = path.join(artifactDir, artifactsFilename)
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
fs.mkdirSync(artifactDir, { recursive: true })
|
|
380
|
+
|
|
381
|
+
if (Array.isArray(flowNodes)) {
|
|
382
|
+
fs.writeFileSync(artifactPath, eol.auto(JSON.stringify(flowNodes, null, 2)), 'utf8')
|
|
383
|
+
} else {
|
|
384
|
+
const monolithPath = path.join(projectPath, cfg.monolithFilename || RED.settings.flowFile || 'flows.json')
|
|
385
|
+
|
|
386
|
+
if (!fs.existsSync(monolithPath)) {
|
|
387
|
+
RED.log.warn(`[node-red-contrib-flow-splitter-extended] Could not write artifact: monolith file not found at '${monolithPath}'`)
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
fs.copyFileSync(monolithPath, artifactPath)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
RED.log.info(`[node-red-contrib-flow-splitter-extended] Wrote monolith artifact at '${artifactPath}'`)
|
|
395
|
+
} catch (error) {
|
|
396
|
+
RED.log.warn(`[node-red-contrib-flow-splitter-extended] Could not write monolith artifact: ${error.message}`)
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
224
400
|
/**
|
|
225
401
|
* Manual reload endpoint handler
|
|
226
|
-
*
|
|
402
|
+
* Restores functions/templates from files and reloads flows
|
|
227
403
|
*/
|
|
228
404
|
async function manualReload(req, res) {
|
|
229
405
|
try {
|
|
@@ -232,7 +408,7 @@ async function manualReload(req, res) {
|
|
|
232
408
|
const projectPath = getProjectPath()
|
|
233
409
|
const cfg = loadSplitterConfig(projectPath)
|
|
234
410
|
|
|
235
|
-
|
|
411
|
+
restoreFunctionsTemplatesIntoSplitFiles(cfg, projectPath)
|
|
236
412
|
|
|
237
413
|
const flowSet = manager.constructFlowSetFromTreeFiles(cfg, projectPath)
|
|
238
414
|
|
|
@@ -245,6 +421,7 @@ async function manualReload(req, res) {
|
|
|
245
421
|
}
|
|
246
422
|
|
|
247
423
|
manager.constructMonolithFileFromFlowSet(flowSet, cfg, projectPath, false)
|
|
424
|
+
writeMonolithArtifact(cfg, projectPath)
|
|
248
425
|
|
|
249
426
|
const PRIVATE_RED = getPrivateRED()
|
|
250
427
|
await PRIVATE_RED.nodes.loadFlows(true)
|
|
@@ -278,9 +455,9 @@ async function onFlowReload(flowEventData) {
|
|
|
278
455
|
|
|
279
456
|
if (flowEventData.config.flows.length === 0) {
|
|
280
457
|
// The flow file does not exist or is empty - rebuild from split files
|
|
281
|
-
RED.log.info("[node-red-contrib-flow-splitter-extended] Rebuilding
|
|
458
|
+
RED.log.info("[node-red-contrib-flow-splitter-extended] Rebuilding single flows.json file from source files")
|
|
282
459
|
|
|
283
|
-
|
|
460
|
+
restoreFunctionsTemplatesIntoSplitFiles(cfg, projectPath)
|
|
284
461
|
|
|
285
462
|
const flowSet = manager.constructFlowSetFromTreeFiles(cfg, projectPath)
|
|
286
463
|
|
|
@@ -291,6 +468,7 @@ async function onFlowReload(flowEventData) {
|
|
|
291
468
|
|
|
292
469
|
const updatedCfg = manager.constructMonolithFileFromFlowSet(flowSet, cfg, projectPath, false)
|
|
293
470
|
writeSplitterConfig(updatedCfg, projectPath)
|
|
471
|
+
writeMonolithArtifact(updatedCfg, projectPath)
|
|
294
472
|
|
|
295
473
|
const PRIVATE_RED = getPrivateRED()
|
|
296
474
|
|
|
@@ -303,10 +481,14 @@ async function onFlowReload(flowEventData) {
|
|
|
303
481
|
}
|
|
304
482
|
|
|
305
483
|
// Flows exist - split into source files
|
|
484
|
+
// First, clean up any old files from renamed tabs/subflows
|
|
485
|
+
cleanupRenamedFlows(flowEventData.config.flows, cfg, projectPath)
|
|
486
|
+
|
|
306
487
|
const flowSet = manager.constructFlowSetFromMonolithObject(flowEventData.config.flows)
|
|
307
488
|
|
|
308
489
|
const updatedCfg = manager.constructTreeFilesFromFlowSet(flowSet, cfg, projectPath)
|
|
309
490
|
writeSplitterConfig(updatedCfg, projectPath)
|
|
491
|
+
writeMonolithArtifact(updatedCfg, projectPath, flowEventData.config.flows)
|
|
310
492
|
|
|
311
493
|
extractFunctionsTemplatesFromSplitFiles(updatedCfg, projectPath)
|
|
312
494
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vdwpsmt/node-red-contrib-flow-splitter-extended",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Split your flows.json file in individual YAML or JSON files (per tab, subflow and config-node) with optional function and ui-template node code extraction.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"keywords": [
|