function importNodes(newNodesObj,options) { // createNewIds,createMissingWorkspace) {
        const defOpts = { generateIds: false, addFlow: false, markChanged: false, reimport: false, importMap: {} }
        options = Object.assign({}, defOpts, options)
        options.importMap = options.importMap || {}
        const createNewIds = options.generateIds;
        const reimport = (!createNewIds && !!options.reimport)
        const createMissingWorkspace = options.addFlow;
        var i;
        var n;
        var newNodes;
        var nodeZmap = {};
        var recoveryWorkspace;
        if (typeof newNodesObj === "string") {
            if (newNodesObj === "") {
                return;
            }
            try {
                newNodes = JSON.parse(newNodesObj);
            } catch(err) {
                var e = new Error(RED._("clipboard.invalidFlow",{message:err.message}));
                e.code = "NODE_RED";
                throw e;
            }
        } else {
            newNodes = newNodesObj;
        }

        if (!Array.isArray(newNodes)) {
            newNodes = [newNodes];
        }

        // Scan for any duplicate nodes and remove them. This is a temporary
        // fix to help resolve corrupted flows caused by 0.20.0 where multiple
        // copies of the flow would get loaded at the same time.
        // If the user hit deploy they would have saved those duplicates.
        var seenIds = {};
        var existingNodes = [];
        var nodesToReplace = [];

        newNodes = newNodes.filter(function(n) {
            var id = n.id;
            if (seenIds[n.id]) {
                return false;
            }
            seenIds[n.id] = true;

            if (!options.generateIds) {
                if (!options.importMap[id]) {
                    // No conflict resolution for this node
                    var existing = allNodes.getNode(id) || configNodes[id] || workspaces[id] || subflows[id] || groups[id] || junctions[id];
                    if (existing) {
                        existingNodes.push({existing:existing, imported:n});
                    }
                } else if (options.importMap[id] === "replace") {
                    nodesToReplace.push(n);
                    return false;
                }
            }

            return true;
        })

        if (existingNodes.length > 0) {
            var errorMessage = RED._("clipboard.importDuplicate",{count:existingNodes.length});
            var nodeList = $("<ul>");
            var existingNodesCount = Math.min(5,existingNodes.length);
            for (var i=0;i<existingNodesCount;i++) {
                var conflict = existingNodes[i];
                $("<li>").text(
                    conflict.existing.id+
                    " [ "+conflict.existing.type+ ((conflict.imported.type !== conflict.existing.type)?" | "+conflict.imported.type:"")+" ]").appendTo(nodeList)
            }
            if (existingNodesCount !== existingNodes.length) {
                $("<li>").text(RED._("deploy.confirm.plusNMore",{count:existingNodes.length-existingNodesCount})).appendTo(nodeList)
            }
            var wrapper = $("<p>").append(nodeList);

            var existingNodesError = new Error(errorMessage+wrapper.html());
            existingNodesError.code = "import_conflict";
            existingNodesError.importConfig = identifyImportConflicts(newNodes);
            throw existingNodesError;
        }
        var removedNodes;
        if (nodesToReplace.length > 0) {
            var replaceResult = replaceNodes(nodesToReplace);
            removedNodes = replaceResult.removedNodes;
        }


        var isInitialLoad = false;
        if (!initialLoad) {
            isInitialLoad = true;
            initialLoad = JSON.parse(JSON.stringify(newNodes));
        }
        var unknownTypes = [];
        for (i=0;i<newNodes.length;i++) {
            n = newNodes[i];
            var id = n.id;
            // TODO: remove workspace in next release+1
            if (n.type != "workspace" &&
                n.type != "tab" &&
                n.type != "subflow" &&
                n.type != "group" &&
                n.type != 'junction' &&
                !registry.getNodeType(n.type) &&
                n.type.substring(0,8) != "subflow:" &&
                unknownTypes.indexOf(n.type)==-1) {
                    unknownTypes.push(n.type);
            }
            if (n.z) {
                nodeZmap[n.z] = nodeZmap[n.z] || [];
                nodeZmap[n.z].push(n);
            } else if (isInitialLoad && n.hasOwnProperty('x') && n.hasOwnProperty('y') && !n.z) {
                // Hit the rare issue where node z values get set to 0.
                // Repair the flow - but we really need to track that down.
                if (!recoveryWorkspace) {
                    recoveryWorkspace = {
                        id: RED.nodes.id(),
                        type: "tab",
                        disabled: false,
                        label: RED._("clipboard.recoveredNodes"),
                        info: RED._("clipboard.recoveredNodesInfo"),
                        env: []
                    }
                    addWorkspace(recoveryWorkspace);
                    RED.workspaces.add(recoveryWorkspace);
                    nodeZmap[recoveryWorkspace.id] = [];
                }
                n.z = recoveryWorkspace.id;
                nodeZmap[recoveryWorkspace.id].push(n);
            }

        }
        if (!isInitialLoad && unknownTypes.length > 0) {
            var typeList = $("<ul>");
            unknownTypes.forEach(function(t) {
                $("<li>").text(t).appendTo(typeList);
            })
            typeList = typeList[0].outerHTML;
            RED.notify("<p>"+RED._("clipboard.importUnrecognised",{count:unknownTypes.length})+"</p>"+typeList,"error",false,10000);
        }

        var activeWorkspace = RED.workspaces.active();
        //TODO: check the z of the subflow instance and check _that_ if it exists
        var activeSubflow = getSubflow(activeWorkspace);
        for (i=0;i<newNodes.length;i++) {
            var m = /^subflow:(.+)$/.exec(newNodes[i].type);
            if (m) {
                var subflowId = m[1];
                var parent = getSubflow(activeWorkspace);
                if (parent) {
                    var err;
                    if (subflowId === parent.id) {
                        err = new Error(RED._("notification.errors.cannotAddSubflowToItself"));
                    }
                    if (subflowContains(subflowId,parent.id)) {
                        err = new Error(RED._("notification.errors.cannotAddCircularReference"));
                    }
                    if (err) {
                        // TODO: standardise error codes
                        err.code = "NODE_RED";
                        throw err;
                    }
                }
            }
        }

        var new_workspaces = [];
        var workspace_map = {};
        var new_subflows = [];
        var subflow_map = {};
        var subflow_denylist = {};
        var node_map = {};
        var new_nodes = [];
        var new_links = [];
        var new_groups = [];
        var new_junctions = [];
        var new_group_set = new Set();
        var nid;
        var def;
        var configNode;
        var missingWorkspace = null;
        var d;

        if (recoveryWorkspace) {
            new_workspaces.push(recoveryWorkspace);
        }

        // Find all tabs and subflow templates
        for (i=0;i<newNodes.length;i++) {
            n = newNodes[i];
            // TODO: remove workspace in next release+1
            if (n.type === "workspace" || n.type === "tab") {
                if (n.type === "workspace") {
                    n.type = "tab";
                }
                if (defaultWorkspace == null) {
                    defaultWorkspace = n;
                }
                if (activeWorkspace === 0) {
                    activeWorkspace = n.id;
                }
                if (createNewIds || options.importMap[n.id] === "copy") {
                    nid = getID();
                    workspace_map[n.id] = nid;
                    n.id = nid;
                } else {
                    workspace_map[n.id] = n.id;
                }
                addWorkspace(n);
                RED.workspaces.add(n);
                new_workspaces.push(n);
            } else if (n.type === "subflow") {
                var matchingSubflow;
                if (!options.importMap[n.id]) {
                    matchingSubflow = checkForMatchingSubflow(n,nodeZmap[n.id]);
                }
                if (matchingSubflow) {
                    subflow_denylist[n.id] = matchingSubflow;
                } else {
                    const oldId = n.id;

                    subflow_map[n.id] = n;
                    if (createNewIds || options.importMap[n.id] === "copy") {
                        nid = getID();
                        n.id = nid;
                    }
                    // TODO: handle createNewIds - map old to new subflow ids
                    n.in.forEach(function(input,i) {
                        input.type = "subflow";
                        input.direction = "in";
                        input.z = n.id;
                        input.i = i;
                        input.id = getID();
                    });
                    n.out.forEach(function(output,i) {
                        output.type = "subflow";
                        output.direction = "out";
                        output.z = n.id;
                        output.i = i;
                        output.id = getID();
                    });
                    if (n.status) {
                        n.status.type = "subflow";
                        n.status.direction = "status";
                        n.status.z = n.id;
                        n.status.id = getID();
                    }
                    new_subflows.push(n);
                    addSubflow(n,createNewIds || options.importMap[oldId] === "copy");
                }
            }
        }

        // Add a tab if there isn't one there already
        if (defaultWorkspace == null) {
            defaultWorkspace = { type:"tab", id:getID(), disabled: false, info:"",  label:RED._('workspace.defaultName',{number:1}), env:[]};
            addWorkspace(defaultWorkspace);
            RED.workspaces.add(defaultWorkspace);
            new_workspaces.push(defaultWorkspace);
            activeWorkspace = RED.workspaces.active();
        }

        const pendingConfigNodes = []
        const pendingConfigNodeIds = new Set()
        // Find all config nodes and add them
        for (i=0;i<newNodes.length;i++) {
            n = newNodes[i];
            def = registry.getNodeType(n.type);
            if (def && def.category == "config") {
                var existingConfigNode = null;
                if (createNewIds || options.importMap[n.id] === "copy") {
                    if (n.z) {
                        if (subflow_denylist[n.z]) {
                            continue;
                        } else if (subflow_map[n.z]) {
                            n.z = subflow_map[n.z].id;
                        } else {
                            n.z = workspace_map[n.z];
                            if (!workspaces[n.z]) {
                                if (createMissingWorkspace) {
                                    if (missingWorkspace === null) {
                                        missingWorkspace = RED.workspaces.add(null,true);
                                        new_workspaces.push(missingWorkspace);
                                    }
                                    n.z = missingWorkspace.id;
                                } else {
                                    n.z = activeWorkspace;
                                }
                            }
                        }
                    }
                    if (options.importMap[n.id] !== "copy") {
                        existingConfigNode = RED.nodes.node(n.id);
                        if (existingConfigNode) {
                            if (n.z && existingConfigNode.z !== n.z) {
                                existingConfigNode = null;
                                // Check the config nodes on n.z
                                for (var cn in configNodes) {
                                    if (configNodes.hasOwnProperty(cn)) {
                                        if (configNodes[cn].z === n.z && compareNodes(configNodes[cn],n,false)) {
                                            existingConfigNode = configNodes[cn];
                                            node_map[n.id] = configNodes[cn];
                                            break;
                                        }
                                    }
                                }
                            }
                        }
                    }
                } else {
                    const keepNodesCurrentZ = reimport && n.z && (RED.workspaces.contains(n.z) || RED.nodes.subflow(n.z))
                    if (!keepNodesCurrentZ && n.z && !workspace_map[n.z] && !subflow_map[n.z]) {
                        n.z = activeWorkspace;
                    }
                }

                if (!existingConfigNode || existingConfigNode._def.exclusive) { //} || !compareNodes(existingConfigNode,n,true) || existingConfigNode.z !== n.z) {
                    configNode = {
                        id:n.id,
                        z:n.z,
                        type:n.type,
                        info: n.info,
                        users:[],
                        _config:{},
                        _configNodeReferences: new Set()
                    };
                    if (!n.z) {
                        delete configNode.z;
                    }
                    if (options.markChanged) {
                        configNode.changed = true
                    }
                    if (n.hasOwnProperty('d')) {
                        configNode.d = n.d;
                    }
                    for (d in def.defaults) {
                        if (def.defaults.hasOwnProperty(d)) {
                            configNode[d] = n[d];
                            configNode._config[d] = JSON.stringify(n[d]);
                            if (def.defaults[d].type) {
                                configNode._configNodeReferences.add(n[d])
                            }
                        }
                    }
                    if (def.hasOwnProperty('credentials') && n.hasOwnProperty('credentials')) {
                        configNode.credentials = {};
                        for (d in def.credentials) {
                            if (def.credentials.hasOwnProperty(d) && n.credentials.hasOwnProperty(d)) {
                                configNode.credentials[d] = n.credentials[d];
                            }
                        }
                    }
                    configNode.label = def.label;
                    configNode._def = def;
                    if (createNewIds || options.importMap[n.id] === "copy") {
                        configNode.id = getID();
                    }
                    node_map[n.id] = configNode;
                    pendingConfigNodes.push(configNode);
                    pendingConfigNodeIds.add(configNode.id)
                }
            }
        }

        // We need to sort new_nodes (which only contains config nodes at this point)
        // to ensure they get added in the right order. If NodeA depends on NodeB, then
        // NodeB must be added first.
        
        // Limit us to 5 full iterations of the list - this should be more than
        // enough to process the list as config->config node relationships are
        // not very common
        let iterationLimit = pendingConfigNodes.length * 5
        const handledConfigNodes = new Set()
        while (pendingConfigNodes.length > 0 && iterationLimit > 0) {
            const node = pendingConfigNodes.shift()
            let hasPending = false
            // Loop through the nodes referenced by this node to see if anything
            // is pending
            node._configNodeReferences.forEach(id => {
                if (pendingConfigNodeIds.has(id) && !handledConfigNodes.has(id)) {
                    // This reference is for a node we know is in this import, but
                    // it isn't added yet - flag as pending
                    hasPending = true
                }
            })
            if (!hasPending) {
                // This node has no pending config node references - safe to add
                delete node._configNodeReferences
                new_nodes.push(node)
                handledConfigNodes.add(node.id)
            } else {
                // This node has pending config node references
                // Put to the back of the queue
                pendingConfigNodes.push(node)
            }
            iterationLimit--
        }
        if (pendingConfigNodes.length > 0) {
            // We exceeded the iteration count. Could be due to reference loops
            // between the config nodes. At this point, just add the remaining
            // nodes as-is
            pendingConfigNodes.forEach(node => {
                delete node._configNodeReferences
                new_nodes.push(node)
            })
        }

        // Find regular flow nodes and subflow instances
        for (i=0;i<newNodes.length;i++) {
            n = newNodes[i];
            // TODO: remove workspace in next release+1
            if (n.type !== "workspace" && n.type !== "tab" && n.type !== "subflow") {
                def = registry.getNodeType(n.type);
                if (!def || def.category != "config") {
                    var node = {
                        x:parseFloat(n.x || 0),
                        y:parseFloat(n.y || 0),
                        z:n.z,
                        type: n.type,
                        info: n.info,
                        changed:false,
                        _config:{}
                    }
                    if (n.type !== "group" && n.type !== 'junction') {
                        node.wires = n.wires||[];
                        node.inputLabels = n.inputLabels;
                        node.outputLabels = n.outputLabels;
                        node.icon = n.icon;
                    }
                    if (n.type === 'junction') {
                        node.wires = n.wires||[];
                    }
                    if (n.hasOwnProperty('l')) {
                        node.l = n.l;
                    }
                    if (n.hasOwnProperty('d')) {
                        node.d = n.d;
                    }
                    if (n.hasOwnProperty('g')) {
                        node.g = n.g;
                    }
                    if (options.markChanged) {
                        node.changed = true
                    }
                    if (createNewIds || options.importMap[n.id] === "copy") {
                        if (subflow_denylist[n.z]) {
                            continue;
                        } else if (subflow_map[node.z]) {
                            node.z = subflow_map[node.z].id;
                        } else {
                            node.z = workspace_map[node.z];
                            if (!workspaces[node.z]) {
                                if (createMissingWorkspace) {
                                    if (missingWorkspace === null) {
                                        missingWorkspace = RED.workspaces.add(null,true);
                                        new_workspaces.push(missingWorkspace);
                                    }
                                    node.z = missingWorkspace.id;
                                } else {
                                    node.z = activeWorkspace;
                                }
                            }
                        }
                        node.id = getID();
                    } else {
                        node.id = n.id;
                        const keepNodesCurrentZ = reimport && node.z && (RED.workspaces.contains(node.z) || RED.nodes.subflow(node.z))
                        if (!keepNodesCurrentZ && (node.z == null || (!workspace_map[node.z] && !subflow_map[node.z]))) {
                            if (createMissingWorkspace) {
                                if (missingWorkspace === null) {
                                    missingWorkspace = RED.workspaces.add(null,true);
                                    new_workspaces.push(missingWorkspace);
                                }
                                node.z = missingWorkspace.id;
                            } else {
                                node.z = activeWorkspace;
                            }
                        }
                    }
                    node._def = def;
                    if (node.type === "group") {
                        node._def = RED.group.def;
                        for (d in node._def.defaults) {
                            if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'outputs') {
                                node[d] = n[d];
                                node._config[d] = JSON.stringify(n[d]);
                            }
                        }
                        node._config.x = node.x;
                        node._config.y = node.y;
                        if (n.hasOwnProperty('w')) {
                            node.w = n.w
                        }
                        if (n.hasOwnProperty('h')) {
                            node.h = n.h
                        }
                    } else if (n.type.substring(0,7) === "subflow") {
                        var parentId = n.type.split(":")[1];
                        var subflow = subflow_denylist[parentId]||subflow_map[parentId]||getSubflow(parentId);
                        if (!subflow){
                            node._def = {
                                color:"#fee",
                                defaults: {},
                                label: "unknown: "+n.type,
                                labelStyle: "red-ui-flow-node-label-italic",
                                outputs: n.outputs|| (n.wires && n.wires.length) || 0,
                                set: registry.getNodeSet("node-red/unknown")
                            }
                            var orig = {};
                            for (var p in n) {
                                if (n.hasOwnProperty(p) && p!="x" && p!="y" && p!="z" && p!="id" && p!="wires") {
                                    orig[p] = n[p];
                                }
                            }
                            node._orig = orig;
                            node.name = n.type;
                            node.type = "unknown";
                        } else {
                            if (subflow_denylist[parentId] || createNewIds || options.importMap[n.id] === "copy") {
                                parentId = subflow.id;
                                node.type = "subflow:"+parentId;
                                node._def = registry.getNodeType(node.type);
                                delete node.i;
                            }
                            node.name = n.name;
                            node.outputs = subflow.out.length;
                            node.inputs = subflow.in.length;
                            node.env = n.env;
                        }
                    } else if (n.type === 'junction') {
                         node._def = {defaults:{}}
                         node._config.x = node.x
                         node._config.y = node.y
                         node.inputs = 1
                         node.outputs = 1
                         node.w = 0;
                         node.h = 0;

                    } else {
                        if (!node._def) {
                            if (node.x && node.y) {
                                node._def = {
                                    color:"#fee",
                                    defaults: {},
                                    label: "unknown: "+n.type,
                                    labelStyle: "red-ui-flow-node-label-italic",
                                    outputs: n.outputs|| (n.wires && n.wires.length) || 0,
                                    set: registry.getNodeSet("node-red/unknown")
                                }
                            } else {
                                node._def = {
                                    category:"config",
                                    set: registry.getNodeSet("node-red/unknown")
                                };
                                node.users = [];
                                // This is a config node, so delete the default
                                // non-config node properties
                                delete node.x;
                                delete node.y;
                                delete node.wires;
                                delete node.inputLabels;
                                delete node.outputLabels;
                                if (!n.z) {
                                    delete node.z;
                                }
                            }
                            var orig = {};
                            for (var p in n) {
                                if (n.hasOwnProperty(p) && p!="x" && p!="y" && p!="z" && p!="id" && p!="wires") {
                                    orig[p] = n[p];
                                }
                            }
                            node._orig = orig;
                            node.name = n.type;
                            node.type = "unknown";
                        }
                        if (node._def.category != "config") {
                            if (n.hasOwnProperty('inputs') && node._def.defaults.hasOwnProperty("inputs")) {
                                node.inputs = parseInt(n.inputs, 10);
                                node._config.inputs = JSON.stringify(n.inputs);
                            } else {
                                node.inputs = node._def.inputs;
                            }
                            if (n.hasOwnProperty('outputs') && node._def.defaults.hasOwnProperty("outputs")) {
                                node.outputs = parseInt(n.outputs, 10);
                                node._config.outputs = JSON.stringify(n.outputs);
                            } else {
                                node.outputs = node._def.outputs;
                            }

                            // The node declares outputs in its defaults, but has not got a valid value
                            // Defer to the length of the wires array
                            if (node.hasOwnProperty('wires')) {
                                if (isNaN(node.outputs)) {
                                    node.outputs = node.wires.length;
                                } else if (node.wires.length > node.outputs) {
                                    // If 'wires' is longer than outputs, clip wires
                                    console.log("Warning: node.wires longer than node.outputs - trimming wires:", node.id, " wires:", node.wires.length, " outputs:", node.outputs);
                                    node.wires = node.wires.slice(0, node.outputs);
                                }
                            }

                            for (d in node._def.defaults) {
                                if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'outputs') {
                                    node[d] = n[d];
                                    node._config[d] = JSON.stringify(n[d]);
                                }
                            }
                            node._config.x = node.x;
                            node._config.y = node.y;
                            if (node._def.hasOwnProperty('credentials') && n.hasOwnProperty('credentials')) {
                                node.credentials = {};
                                for (d in node._def.credentials) {
                                    if (node._def.credentials.hasOwnProperty(d) && n.credentials.hasOwnProperty(d)) {
                                        node.credentials[d] = n.credentials[d];
                                    }
                                }
                            }
                        }
                    }
                    node_map[n.id] = node;
                    // If an 'unknown' config node, it will not have been caught by the
                    // proper config node handling, so needs adding to new_nodes here
                    if (node.type === 'junction') {
                        new_junctions.push(node)
                    } else if (node.type === "unknown" || node._def.category !== "config") {
                        new_nodes.push(node);
                    } else if (node.type === "group") {
                        new_groups.push(node);
                        new_group_set.add(node.id);
                    }
                }
            }
        }

        // Remap all wires and config node references
        for (i=0;i<new_nodes.length+new_junctions.length;i++) {
            if (i<new_nodes.length) {
                n = new_nodes[i];
            } else {
                n = new_junctions[i - new_nodes.length]
            }
            if (n.wires) {
                for (var w1=0;w1<n.wires.length;w1++) {
                    var wires = (Array.isArray(n.wires[w1]))?n.wires[w1]:[n.wires[w1]];
                    for (var w2=0;w2<wires.length;w2++) {
                        if (node_map.hasOwnProperty(wires[w2])) {
                            if (n.z === node_map[wires[w2]].z) {
                                var link = {source:n,sourcePort:w1,target:node_map[wires[w2]]};
                                addLink(link);
                                new_links.push(link);
                            } else {
                                console.log("Warning: dropping link that crosses tabs:",n.id,"->",node_map[wires[w2]].id);
                            }
                        }
                    }
                }
                delete n.wires;
            }
            if (n.g && node_map[n.g]) {
                n.g = node_map[n.g].id;
            } else {
                delete n.g
            }
            // If importing a link node, ensure both ends of each link are either:
            // - not in a subflow
            // - both in the same subflow (not for link call node)
            if (/^link /.test(n.type) && n.links) {
                n.links = n.links.filter(function(id) {
                    const otherNode = node_map[id] || RED.nodes.node(id);
                    if (!otherNode) {
                        // Cannot find other end - remove the link
                        return false
                    }
                    if (otherNode.z === n.z) {
                        // Both ends in the same flow/subflow
                        return true
                    } else if (n.type === "link call" && !getSubflow(otherNode.z)) {
                        // Link call node can call out of a subflow as long as otherNode is
                        // not in a subflow
                        return true
                    } else if (!!getSubflow(n.z) || !!getSubflow(otherNode.z)) {
                        // One end is in a subflow - remove the link
                        return false
                    }
                    return true
                });
            }
            for (var d3 in n._def.defaults) {
                if (n._def.defaults.hasOwnProperty(d3)) {
                    if (n._def.defaults[d3].type) {
                        var nodeList = n[d3];
                        if (!Array.isArray(nodeList)) {
                            nodeList = [nodeList];
                        }
                        nodeList = nodeList.map(function(id) {
                            var node = node_map[id];
                            if (node) {
                                return node.id;
                            }
                            return id;
                        })
                        n[d3] = Array.isArray(n[d3])?nodeList:nodeList[0];
                    }
                }
            }
        }
        for (i=0;i<new_subflows.length;i++) {
            n = new_subflows[i];
            n.in.forEach(function(input) {
                input.wires.forEach(function(wire) {
                    if (node_map.hasOwnProperty(wire.id)) {
                        var link = {source:input, sourcePort:0, target:node_map[wire.id]};
                        addLink(link);
                        new_links.push(link);
                    }
                });
                delete input.wires;
            });
            n.out.forEach(function(output) {
                output.wires.forEach(function(wire) {
                    var link;
                    if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) {
                        link = {source:n.in[wire.port], sourcePort:wire.port,target:output};
                    } else if (node_map.hasOwnProperty(wire.id) || subflow_map.hasOwnProperty(wire.id)) {
                        link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:output};
                    }
                    if (link) {
                        addLink(link);
                        new_links.push(link);
                    }
                });
                delete output.wires;
            });
            if (n.status) {
                n.status.wires.forEach(function(wire) {
                    var link;
                    if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) {
                        link = {source:n.in[wire.port], sourcePort:wire.port,target:n.status};
                    } else if (node_map.hasOwnProperty(wire.id) || subflow_map.hasOwnProperty(wire.id)) {
                        link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:n.status};
                    }
                    if (link) {
                        addLink(link);
                        new_links.push(link);
                    }
                });
                delete n.status.wires;
            }
        }
        // Order the groups to ensure they are outer-most to inner-most
        var groupDepthMap = {};
        for (i=0;i<new_groups.length;i++) {
            n = new_groups[i];

            if (n.g && !new_group_set.has(n.g)) {
                delete n.g;
            }
            if (!n.g) {
                groupDepthMap[n.id] = 0;
            }
        }
        var changedDepth;
        do {
            changedDepth = false;
            for (i=0;i<new_groups.length;i++) {
                n = new_groups[i];
                if (n.g) {
                    if (groupDepthMap[n.id] !== groupDepthMap[n.g] + 1) {
                        groupDepthMap[n.id] = groupDepthMap[n.g] + 1;
                        changedDepth = true;
                    }
                }
            }
        } while(changedDepth);

        new_groups.sort(function(A,B) {
            return groupDepthMap[A.id] - groupDepthMap[B.id];
        });
        for (i=0;i<new_groups.length;i++) {
            new_groups[i] = addGroup(new_groups[i]);
            node_map[new_groups[i].id] = new_groups[i]
        }

        for (i=0;i<new_junctions.length;i++) {
            new_junctions[i] = addJunction(new_junctions[i]);
            node_map[new_junctions[i].id] = new_junctions[i]
        }


        // Now the nodes have been fully updated, add them.
        for (i=0;i<new_nodes.length;i++) {
            new_nodes[i] = addNode(new_nodes[i])
            node_map[new_nodes[i].id] = new_nodes[i]
        }

        // Finally validate them all.
        // This has to be done after everything is added so that any checks for
        // dependent config nodes will pass
        for (i=0;i<new_nodes.length;i++) {
            var node = new_nodes[i];
            RED.editor.validateNode(node);
        }
        const lookupNode = (id) => {
            const mappedNode = node_map[id]
            if (!mappedNode) {
                return null
            }
            if (mappedNode.__isProxy__) {
                return mappedNode
            } else {
                return node_map[mappedNode.id]
            }
        }
        // Update groups to reference proxy node objects
        for (i=0;i<new_groups.length;i++) {
            n = new_groups[i];
            // bypass the proxy in case the flow is locked
            n.__node__.nodes = n.nodes.map(lookupNode)
            // Just in case the group references a node that doesn't exist for some reason
            n.__node__.nodes = n.nodes.filter(function(v) {
                if (v) {
                    // Repair any nodes that have forgotten they are in this group
                    if (v.g !== n.id) {
                        v.g = n.id;
                    }
                }
                return !!v
            });
        }

        // Update links to use proxy node objects
        for (i=0;i<new_links.length;i++) {
            new_links[i].source = lookupNode(new_links[i].source.id) || new_links[i].source
            new_links[i].target = lookupNode(new_links[i].target.id) || new_links[i].target
        }

        RED.workspaces.refresh();


        if (recoveryWorkspace) {
            var notification = RED.notify(RED._("clipboard.recoveredNodesNotification",{flowName:RED._("clipboard.recoveredNodes")}),{
                type:"warning",
                fixed:true,
                buttons: [
                    {text: RED._("common.label.close"), click: function() { notification.close() }}
                ]
            });
        }

        return {
            nodes:new_nodes,
            links:new_links,
            groups:new_groups,
            junctions: new_junctions,
            workspaces:new_workspaces,
            subflows:new_subflows,
            missingWorkspace: missingWorkspace,
            removedNodes: removedNodes
        }
    }
Options:
 - generateIds - whether to replace all node ids
 - addFlow - whether to import nodes to a new tab
 - markChanged - whether to set changed=true on all newly imported objects
 - reimport - if node has a .z property, dont overwrite it
              Only applicible when `generateIds` is false
 - importMap - how to resolve any conflicts.
      - id:import - import as-is
      - id:copy - import with new id
      - id:replace - import over the top of existing