<commit_before>'use strict';
/* Controller for Fragment Sankey Diagram View */
angular.module('lcaApp.fragment.sankey',
                ['ui.router', 'lcaApp.sankey', 'lcaApp.resources.service', 'lcaApp.status.service',
                 'lcaApp.format', 'lcaApp.fragmentNavigation.service', 'lcaApp.models.scenario'])
    .controller('FragmentSankeyCtrl',
        ['$scope', '$stateParams', '$state', 'StatusService', '$q', '$log',
        'ScenarioModelService', 'FragmentService', 'FragmentFlowService', 'FlowForFragmentService', 'ProcessService',
        'FlowPropertyForFragmentService', 'FormatService', 'FragmentNavigationService', 'MODEL_BASE_CASE_SCENARIO_ID',
        function ($scope, $stateParams, $state, StatusService, $q, $log, ScenarioModelService, FragmentService,
                  FragmentFlowService, FlowForFragmentService, ProcessService, FlowPropertyForFragmentService,
                  FormatService, FragmentNavigationService, MODEL_BASE_CASE_SCENARIO_ID) {
            var fragmentID,
                scenarioID = MODEL_BASE_CASE_SCENARIO_ID,
            //
                graph = {},
                reverseIndex = {},  // map fragmentFlowID to graph.nodes and graph.links
                baseValue = 1E-14,  // sankey link base value (replaces 0).
                magFormat = FormatService.format();

            $scope.color = { domain: (["Fragment", "InputOutput", "Cutoff", "Process", "Background"]), range: colorbrewer.Set3[5], property: "nodeType" };
            $scope.selectedFlowProperty = null;
            $scope.selectedNode = null;
            $scope.mouseOverNode = null;
            $scope.fragment = null;
            $scope.scenario = null;
            $scope.$watch("selectedNode", onNodeSelectionChange);
            $scope.$watch("mouseOverNode", onMouseOverNode);

            $scope.onScenarioChange = function() {
                scenarioID = $scope.scenario.scenarioID;
                ScenarioModelService.setActiveID(scenarioID);
                getData();
            };

            $scope.viewFragmentFlowParam = function () {
                $state.go(".fragment-flow-param",
                    {scenarioID: scenarioID, fragmentID: fragmentID});
            };

            /**
             * Temporary workaround for flows with problem properties. Hide them
             * @param fpID
             * @returns {boolean}
             */
            function showFlowProperty(fpID) {
                var fp = FlowPropertyForFragmentService.get(fpID);
                return !(fp["referenceUnit"] === "kgP" || fp["referenceUnit"] === "kg-Av" || fp["referenceUnit"] === "MJ-Av");
            }

            function filterFragmentFlow(ff) {
                return ff.nodeType !== "Cutoff" && ff.flowPropertyMagnitudes &&
                    showFlowProperty(ff.flowPropertyMagnitudes[0]["flowPropertyID"]);
            }

            /**
             * Build sankey graph from loaded data
             * @param {Boolean} makeNew  Indicates if new graph should be created. False means update existing graph.
             */
            function buildGraph(makeNew) {
                try {
                    var ff = FragmentFlowService.getAll(),
                        fragmentFlows = ff.filter (filterFragmentFlow);
                    graph.isNew = makeNew;
                    if (makeNew) {
                        reverseIndex = {};
                        graph.nodes = [];
                        graph.nodes.push(createRootNode());
                        // Add a node for every flow
                        fragmentFlows.forEach(addGraphNode);
                    }
                    // Add a link for every flow. source and target are indexes into nodes array.
                    graph.links = [];
                    fragmentFlows.forEach(addGraphLink);
                    $scope.graph = graph;
                }
                catch (err) {
                    StatusService.handleFailure(err);
                }
            }

            /**
             * Get magnitude of link with a flow property
             * @param {{fragmentFlowID:Number, parentFragmentFlowID:Number, direction: String, flowPropertyMagnitudes:Array}}  link
             * @param {Number}  flowPropertyID    flow property key
             * @return {Number} The magnitude, if link has the flow property. Otherwise, null.
             */
            function getMagnitude(link, flowPropertyID) {
                var magnitude = null, flowPropertyMagnitudes = [];
                if ("flowPropertyMagnitudes" in link) {
                    flowPropertyMagnitudes = link.flowPropertyMagnitudes.filter(
                        /**
                         * @param {{flowPropertyID:number}}  lm
                         */
                            function (lm) {
                            return +lm.flowPropertyID === flowPropertyID;
                        });
                }
                if (flowPropertyMagnitudes && flowPropertyMagnitudes.length > 0) {
                    magnitude = flowPropertyMagnitudes[0].magnitude *  $scope.fragment.activityLevel;
                }
                return magnitude;
            }

            function createRootNode() {
                return {
                    nodeType: "InputOutput",
                    nodeID: 0,
                    nodeName: "Reference Flow",
                    toolTip: "InputOutput"
                }
            }

            /**
             * Add graph node for fragment flow element
             * @param {{fragmentFlowID:number}} element
             */
            function addGraphNode(element) {
                var node = {
                        nodeType: element.nodeType,
                        nodeID: element.fragmentFlowID,
                        nodeName: "",
                        toolTip: ""
                    },
                    fragFlow = FragmentFlowService.get(element.fragmentFlowID),
                    refObj , selectTip
                    ;

                if (fragFlow) {
                    node.nodeName = fragFlow["shortName"];
                }
                if (node.nodeType) {
                    node.toolTip = "" + node.nodeType + "";
                }
                if ("processID" in element) {
                    refObj = ProcessService.get(element.processID);
                    node.selectable = true;
                    selectTip = "Click to view process instance";
                } else if ("subFragmentID" in element) {
                    refObj = FragmentService.get(element.subFragmentID);
                    node.selectable = true;
                    selectTip = "Click to descend";
                }
                if (refObj) {
                    node.toolTip = node.toolTip + "
" + refObj.name + "
";
                }
                if (selectTip) {
                    node.toolTip = node.toolTip + "" + selectTip + "";
                }

                reverseIndex[element.fragmentFlowID] = graph.nodes.push(node) - 1;
            }

            /**
             * Add graph link for fragmentflow element
             * @param {{fragmentFlowID:Number, parentFragmentFlowID:Number, direction:String, flowPropertyMagnitudes:Array}} element
             * @exception Error thrown if referenced flow not found
             */
            function addGraphLink(element) {
                var link, parentIndex,
                    nodeIndex = reverseIndex[element.fragmentFlowID],
                    magnitude = getMagnitude(element, $scope.selectedFlowProperty["flowPropertyID"]),
                    value = (magnitude === null || magnitude <= 0) ? baseValue : baseValue + magnitude,
                    flow = FlowForFragmentService.get(element.flowID),
                    unit = $scope.selectedFlowProperty["referenceUnit"];

                if ("parentFragmentFlowID" in element) {
                    if (element.parentFragmentFlowID in reverseIndex) {
                        parentIndex = reverseIndex[element.parentFragmentFlowID];
                    }
                    else {
                        return;
                    }
                } else {
                    parentIndex = 0;
                }
                link = {
                    nodeID: element.fragmentFlowID,
                    flowID: element.flowID,
                    value: value
                };
                if (flow) {
                    if (magnitude) {
                        link.magnitude = magnitude;
                        link.toolTip = flow.name + " : " + magFormat(magnitude) + " " + unit;
                    } else {
                        link.toolTip = flow.name + " does not have property : " + $scope.selectedFlowProperty["name"];
                    }
                } else {
                    throw new Error ("Flow with ID, " + flowID + ", was not found.");
                }
                if (element.direction === "Input") {
                    link.source = nodeIndex;
                    link.target = parentIndex;
                } else {
                    link.source = parentIndex;
                    link.target = nodeIndex;
                }
                graph.links.push(link);
            }

            /**
             * Prepare fragment data for visualization
             */
            function visualizeFragment() {
                setFlowProperties();
                buildGraph(true);
                StatusService.stopWaiting();
            }

            /**
             * If returning from another view, restore last fragment in navigation stack.
             * Otherwise, set to top-level fragment, activity level derived from scenario.
             */
            function initScopeFragment() {
                var fragState = FragmentNavigationService.getLast();
                if (fragState) {
                    $scope.fragment = fragState;
                    fragmentID = $scope.fragment.fragmentID;
                } else {
                    $scope.fragment = FragmentService.get(fragmentID);
                    if ($scope.fragment) {
                        $scope.fragment.activityLevel = $scope.scenario["activityLevel"];
                        FragmentNavigationService.add($scope.fragment);
                    } else {
                        StatusService.handleFailure("Invalid fragment ID : " + fragmentID);
                    }
                }
                return $scope.fragment;
            }

            /**
             * Prepare to display sub-fragment
             * Calculate sub-fragment activity level, push current fragment on breadcrumb stack
             * @param {{subFragmentID: Number, nodeWeight: Number}} fragmentFlow  containing selected sub-fragment
             */
            function navigateSubFragment( fragmentFlow) {
                var subFragment = FragmentService.get(fragmentFlow.subFragmentID);
                if (subFragment) {
                    subFragment.activityLevel = $scope.fragment.activityLevel * fragmentFlow.nodeWeight;
                    fragmentID = fragmentFlow.subFragmentID;
                    $scope.fragment = subFragment;
                    FragmentNavigationService.add($scope.fragment);
                    getDataForFragment();
                } else {
                    StatusService.handleFailure("Invalid sub-fragment ID : " + fragmentFlow.subFragmentID);
                }
            }

            /**
             * Function called after requests for resources have been fulfilled.
             */
            function handleSuccess() {
                $scope.scenarios = ScenarioModelService.getAll();
                $scope.scenario = ScenarioModelService.get(scenarioID);
                if ($scope.scenario) {
                    fragmentID = $scope.scenario.topLevelFragmentID;
                    ScenarioModelService.setActiveID(scenarioID);
                    $scope.navigationService = FragmentNavigationService.setContext(scenarioID, fragmentID);
                    initScopeFragment();
                    getDataForFragment();
                } else {
                    StatusService.handleFailure("Invalid scenarioID: " + scenarioID);
                }
            }

            /**
             * Update scope with flow properties and select one
             */
            function setFlowProperties() {
                //
                //  If the last flow property selection is not in the current list, reset to the default flow property,
                //  if that is in the list. Otherwise, set to first element in resource payload.
                //
                var selectedFlowProperty = $scope.selectedFlowProperty,
                    flowProperties = FlowPropertyForFragmentService.getAll();
                flowProperties.sort(FlowPropertyForFragmentService.compareByName);
                if (flowProperties) {
                    if (selectedFlowProperty) {
                        selectedFlowProperty = flowProperties.find(function (element) {
                            return (element["flowPropertyID"] === selectedFlowProperty["flowPropertyID"]);
                        });
                    }
                    if (!selectedFlowProperty) {
                        selectedFlowProperty = flowProperties.find(function (element) {
                            return (element.name === "Mass");
                        });
                        if (!selectedFlowProperty) {
                            selectedFlowProperty = flowProperties[0];
                        }
                    }
                } else {
                    selectedFlowProperty = null;
                }
                $scope.flowProperties = flowProperties;
                $scope.selectedFlowProperty = selectedFlowProperty;
            }

            /**
             * Get all data resources
             */
            function getData() {
                StatusService.startWaiting();
                $q.all([ScenarioModelService.load(), FragmentService.load(), ProcessService.load()])
                    .then(handleSuccess,
                    StatusService.handleFailure);
            }

            /**
             * Get data resources that are filtered by fragment.
             * Called after fragment selection changes.
             * If successful, visualize selected fragment.
             */
            function getDataForFragment() {
                StatusService.startWaiting();
                $q.all([FlowPropertyForFragmentService.load({fragmentID: fragmentID}),
                    FragmentFlowService.load({scenarioID: scenarioID, fragmentID: fragmentID}),
                    FlowForFragmentService.load({fragmentID: fragmentID})])
                    .then(visualizeFragment,
                    StatusService.handleFailure);
            }

            /**
             * Called when flow property selection changes.
             * Updates existing sankey graph.
             */
            $scope.onFlowPropertyChange = function () {
                buildGraph(false);
            };

            /**
             * Called when a parent fragment is selected from fragment breadcrumbs.
             * Updates fragment breadcrumbs and gets new fragment data.
             * @param fragment  Fragment selected
             * @param index     Breadcrumb index
             */
            $scope.onParentFragmentSelected = function (fragment, index) {
                fragmentID = fragment.fragmentID;
                $scope.fragment = fragment;
                FragmentNavigationService.setLast(index);
                getDataForFragment();
            };

            /**
             * Called when a node in sankey directive is selected.
             * The node can represent either a fragment or a process.
             * @param newVal    The selected node
             */
            function onNodeSelectionChange(newVal) {
                if (newVal) {
                    var fragmentFlow = FragmentFlowService.get(newVal.nodeID);
                    $log.info("Clicked on node with weight = " + fragmentFlow.nodeWeight);
                    switch (newVal.nodeType) {
                        case "Process" :
                            $state.go("fragment-sankey.process-instance", {
                                    scenarioID: scenarioID,
                                    fragmentID: fragmentID,
                                    fragmentFlowID: fragmentFlow.fragmentFlowID,
                                    activity: $scope.fragment.activityLevel *
                                    fragmentFlow.nodeWeight
                                }
                            );
                            break;
                        case "Fragment" :
                            navigateSubFragment(fragmentFlow);
                            break;
                    }
                }
            }

            /**
             * Get flow table content
             * For each Sankey link provided, get
             * flow name, magnitude and unit associated with selected flow property.
             * If the link does have the selected flow property, display
             * magnitude and unit for the
             * reference flow property.
             *
             * @param {Array}  nodeLinks     Sankey links
             * @return {Array}  Data for display in table
             */
            function getFlowRows(nodeLinks) {
                var flowData = [];

                nodeLinks.forEach( function (l) {
                    if ("flowID" in l) {
                        var flow = FlowForFragmentService.get(l.flowID),
                            flowPropertyID = $scope.selectedFlowProperty["flowPropertyID"],
                            magnitude = l.magnitude,
                            unit = "";
                        if (magnitude) {
                            unit = $scope.selectedFlowProperty["referenceUnit"];
                        } else  {
                            var fp, ff;
                            ff = FragmentFlowService.get(l.nodeID);
                            flowPropertyID = flow["referenceFlowPropertyID"];
                            magnitude = getMagnitude(ff, flowPropertyID);
                            fp = FlowPropertyForFragmentService.get(flowPropertyID);
                            if (fp) {
                               unit = fp["referenceUnit"];
                            }
                        }
                        flowData.push({ name: flow.name, magnitude: magnitude, unit: unit });
                    }
                });
                return flowData;
            }

            function onMouseOverNode(node) {
                if (node) {
                    $scope.inputFlows = getFlowRows(node.targetLinks);
                    $scope.outputFlows = getFlowRows(node.sourceLinks);
                }
            }

            function getActiveScenarioID() {
                var activeID = ScenarioModelService.getActiveID();
                if (activeID) {
                    scenarioID = activeID;
                }
            }

            function getStateParams() {
                if ( $stateParams.hasOwnProperty("scenarioID") && $stateParams.scenarioID !== undefined) {
                    scenarioID = +$stateParams.scenarioID;
                }
                if ($stateParams.hasOwnProperty("fragmentID") && $stateParams.fragmentID !== undefined) {
                    fragmentID = +$stateParams.fragmentID;
                }
            }

            getActiveScenarioID();
            getStateParams();
            getData();
        }]);<commit_msg>Vis: Ignore missing fragment flow property (fixes #192)<commit_after>'use strict';
/* Controller for Fragment Sankey Diagram View */
angular.module('lcaApp.fragment.sankey',
                ['ui.router', 'lcaApp.sankey', 'lcaApp.resources.service', 'lcaApp.status.service',
                 'lcaApp.format', 'lcaApp.fragmentNavigation.service', 'lcaApp.models.scenario'])
    .controller('FragmentSankeyCtrl',
        ['$scope', '$stateParams', '$state', 'StatusService', '$q', '$log',
        'ScenarioModelService', 'FragmentService', 'FragmentFlowService', 'FlowForFragmentService', 'ProcessService',
        'FlowPropertyForFragmentService', 'FormatService', 'FragmentNavigationService', 'MODEL_BASE_CASE_SCENARIO_ID',
        function ($scope, $stateParams, $state, StatusService, $q, $log, ScenarioModelService, FragmentService,
                  FragmentFlowService, FlowForFragmentService, ProcessService, FlowPropertyForFragmentService,
                  FormatService, FragmentNavigationService, MODEL_BASE_CASE_SCENARIO_ID) {
            var fragmentID,
                scenarioID = MODEL_BASE_CASE_SCENARIO_ID,
            //
                graph = {},
                reverseIndex = {},  // map fragmentFlowID to graph.nodes and graph.links
                baseValue = 1E-14,  // sankey link base value (replaces 0).
                magFormat = FormatService.format();

            $scope.color = { domain: (["Fragment", "InputOutput", "Cutoff", "Process", "Background"]), range: colorbrewer.Set3[5], property: "nodeType" };
            $scope.selectedFlowProperty = null;
            $scope.selectedNode = null;
            $scope.mouseOverNode = null;
            $scope.fragment = null;
            $scope.scenario = null;
            $scope.$watch("selectedNode", onNodeSelectionChange);
            $scope.$watch("mouseOverNode", onMouseOverNode);

            $scope.onScenarioChange = function() {
                scenarioID = $scope.scenario.scenarioID;
                ScenarioModelService.setActiveID(scenarioID);
                getData();
            };

            $scope.viewFragmentFlowParam = function () {
                $state.go(".fragment-flow-param",
                    {scenarioID: scenarioID, fragmentID: fragmentID});
            };

            /**
             * Temporary workaround for flows with problem properties. Hide them
             * @param fpID
             * @returns {boolean}
             */
            function showFlowProperty(fpID) {
                var fp = FlowPropertyForFragmentService.get(fpID);
                return fp &&
                    !(fp["referenceUnit"] === "kgP" || fp["referenceUnit"] === "kg-Av" || fp["referenceUnit"] === "MJ-Av");
            }

            function filterFragmentFlow(ff) {
                return ff.nodeType !== "Cutoff" && ff.flowPropertyMagnitudes &&
                    showFlowProperty(ff.flowPropertyMagnitudes[0]["flowPropertyID"]);
            }

            /**
             * Build sankey graph from loaded data
             * @param {Boolean} makeNew  Indicates if new graph should be created. False means update existing graph.
             */
            function buildGraph(makeNew) {
                try {
                    var ff = FragmentFlowService.getAll(),
                        fragmentFlows = ff.filter (filterFragmentFlow);
                    graph.isNew = makeNew;
                    if (makeNew) {
                        reverseIndex = {};
                        graph.nodes = [];
                        graph.nodes.push(createRootNode());
                        // Add a node for every flow
                        fragmentFlows.forEach(addGraphNode);
                    }
                    // Add a link for every flow. source and target are indexes into nodes array.
                    graph.links = [];
                    fragmentFlows.forEach(addGraphLink);
                    $scope.graph = graph;
                }
                catch (err) {
                    StatusService.handleFailure(err);
                }
            }

            /**
             * Get magnitude of link with a flow property
             * @param {{fragmentFlowID:Number, parentFragmentFlowID:Number, direction: String, flowPropertyMagnitudes:Array}}  link
             * @param {Number}  flowPropertyID    flow property key
             * @return {Number} The magnitude, if link has the flow property. Otherwise, null.
             */
            function getMagnitude(link, flowPropertyID) {
                var magnitude = null, flowPropertyMagnitudes = [];
                if ("flowPropertyMagnitudes" in link) {
                    flowPropertyMagnitudes = link.flowPropertyMagnitudes.filter(
                        /**
                         * @param {{flowPropertyID:number}}  lm
                         */
                            function (lm) {
                            return +lm.flowPropertyID === flowPropertyID;
                        });
                }
                if (flowPropertyMagnitudes && flowPropertyMagnitudes.length > 0) {
                    magnitude = flowPropertyMagnitudes[0].magnitude *  $scope.fragment.activityLevel;
                }
                return magnitude;
            }

            function createRootNode() {
                return {
                    nodeType: "InputOutput",
                    nodeID: 0,
                    nodeName: "Reference Flow",
                    toolTip: "InputOutput"
                }
            }

            /**
             * Add graph node for fragment flow element
             * @param {{fragmentFlowID:number}} element
             */
            function addGraphNode(element) {
                var node = {
                        nodeType: element.nodeType,
                        nodeID: element.fragmentFlowID,
                        nodeName: "",
                        toolTip: ""
                    },
                    fragFlow = FragmentFlowService.get(element.fragmentFlowID),
                    refObj , selectTip
                    ;

                if (fragFlow) {
                    node.nodeName = fragFlow["shortName"];
                }
                if (node.nodeType) {
                    node.toolTip = "" + node.nodeType + "";
                }
                if ("processID" in element) {
                    refObj = ProcessService.get(element.processID);
                    node.selectable = true;
                    selectTip = "Click to view process instance";
                } else if ("subFragmentID" in element) {
                    refObj = FragmentService.get(element.subFragmentID);
                    node.selectable = true;
                    selectTip = "Click to descend";
                }
                if (refObj) {
                    node.toolTip = node.toolTip + "
" + refObj.name + "
";
                }
                if (selectTip) {
                    node.toolTip = node.toolTip + "" + selectTip + "";
                }

                reverseIndex[element.fragmentFlowID] = graph.nodes.push(node) - 1;
            }

            /**
             * Add graph link for fragmentflow element
             * @param {{fragmentFlowID:Number, parentFragmentFlowID:Number, direction:String, flowPropertyMagnitudes:Array}} element
             * @exception Error thrown if referenced flow not found
             */
            function addGraphLink(element) {
                var link, parentIndex,
                    nodeIndex = reverseIndex[element.fragmentFlowID],
                    magnitude = getMagnitude(element, $scope.selectedFlowProperty["flowPropertyID"]),
                    value = (magnitude === null || magnitude <= 0) ? baseValue : baseValue + magnitude,
                    flow = FlowForFragmentService.get(element.flowID),
                    unit = $scope.selectedFlowProperty["referenceUnit"];

                if ("parentFragmentFlowID" in element) {
                    if (element.parentFragmentFlowID in reverseIndex) {
                        parentIndex = reverseIndex[element.parentFragmentFlowID];
                    }
                    else {
                        return;
                    }
                } else {
                    parentIndex = 0;
                }
                link = {
                    nodeID: element.fragmentFlowID,
                    flowID: element.flowID,
                    value: value
                };
                if (flow) {
                    if (magnitude) {
                        link.magnitude = magnitude;
                        link.toolTip = flow.name + " : " + magFormat(magnitude) + " " + unit;
                    } else {
                        link.toolTip = flow.name + " does not have property : " + $scope.selectedFlowProperty["name"];
                    }
                } else {
                    throw new Error ("Flow with ID, " + flowID + ", was not found.");
                }
                if (element.direction === "Input") {
                    link.source = nodeIndex;
                    link.target = parentIndex;
                } else {
                    link.source = parentIndex;
                    link.target = nodeIndex;
                }
                graph.links.push(link);
            }

            /**
             * Prepare fragment data for visualization
             */
            function visualizeFragment() {
                setFlowProperties();
                buildGraph(true);
                StatusService.stopWaiting();
            }

            /**
             * If returning from another view, restore last fragment in navigation stack.
             * Otherwise, set to top-level fragment, activity level derived from scenario.
             */
            function initScopeFragment() {
                var fragState = FragmentNavigationService.getLast();
                if (fragState) {
                    $scope.fragment = fragState;
                    fragmentID = $scope.fragment.fragmentID;
                } else {
                    $scope.fragment = FragmentService.get(fragmentID);
                    if ($scope.fragment) {
                        $scope.fragment.activityLevel = $scope.scenario["activityLevel"];
                        FragmentNavigationService.add($scope.fragment);
                    } else {
                        StatusService.handleFailure("Invalid fragment ID : " + fragmentID);
                    }
                }
                return $scope.fragment;
            }

            /**
             * Prepare to display sub-fragment
             * Calculate sub-fragment activity level, push current fragment on breadcrumb stack
             * @param {{subFragmentID: Number, nodeWeight: Number}} fragmentFlow  containing selected sub-fragment
             */
            function navigateSubFragment( fragmentFlow) {
                var subFragment = FragmentService.get(fragmentFlow.subFragmentID);
                if (subFragment) {
                    subFragment.activityLevel = $scope.fragment.activityLevel * fragmentFlow.nodeWeight;
                    fragmentID = fragmentFlow.subFragmentID;
                    $scope.fragment = subFragment;
                    FragmentNavigationService.add($scope.fragment);
                    getDataForFragment();
                } else {
                    StatusService.handleFailure("Invalid sub-fragment ID : " + fragmentFlow.subFragmentID);
                }
            }

            /**
             * Function called after requests for resources have been fulfilled.
             */
            function handleSuccess() {
                $scope.scenarios = ScenarioModelService.getAll();
                $scope.scenario = ScenarioModelService.get(scenarioID);
                if ($scope.scenario) {
                    fragmentID = $scope.scenario.topLevelFragmentID;
                    ScenarioModelService.setActiveID(scenarioID);
                    $scope.navigationService = FragmentNavigationService.setContext(scenarioID, fragmentID);
                    initScopeFragment();
                    getDataForFragment();
                } else {
                    StatusService.handleFailure("Invalid scenarioID: " + scenarioID);
                }
            }

            /**
             * Update scope with flow properties and select one
             */
            function setFlowProperties() {
                //
                //  If the last flow property selection is not in the current list, reset to the default flow property,
                //  if that is in the list. Otherwise, set to first element in resource payload.
                //
                var selectedFlowProperty = $scope.selectedFlowProperty,
                    flowProperties = FlowPropertyForFragmentService.getAll();
                flowProperties.sort(FlowPropertyForFragmentService.compareByName);
                if (flowProperties) {
                    if (selectedFlowProperty) {
                        selectedFlowProperty = flowProperties.find(function (element) {
                            return (element["flowPropertyID"] === selectedFlowProperty["flowPropertyID"]);
                        });
                    }
                    if (!selectedFlowProperty) {
                        selectedFlowProperty = flowProperties.find(function (element) {
                            return (element.name === "Mass");
                        });
                        if (!selectedFlowProperty) {
                            selectedFlowProperty = flowProperties[0];
                        }
                    }
                } else {
                    selectedFlowProperty = null;
                }
                $scope.flowProperties = flowProperties;
                $scope.selectedFlowProperty = selectedFlowProperty;
            }

            /**
             * Get all data resources
             */
            function getData() {
                StatusService.startWaiting();
                $q.all([ScenarioModelService.load(), FragmentService.load(), ProcessService.load()])
                    .then(handleSuccess,
                    StatusService.handleFailure);
            }

            /**
             * Get data resources that are filtered by fragment.
             * Called after fragment selection changes.
             * If successful, visualize selected fragment.
             */
            function getDataForFragment() {
                StatusService.startWaiting();
                $q.all([FlowPropertyForFragmentService.load({fragmentID: fragmentID}),
                    FragmentFlowService.load({scenarioID: scenarioID, fragmentID: fragmentID}),
                    FlowForFragmentService.load({fragmentID: fragmentID})])
                    .then(visualizeFragment,
                    StatusService.handleFailure);
            }

            /**
             * Called when flow property selection changes.
             * Updates existing sankey graph.
             */
            $scope.onFlowPropertyChange = function () {
                buildGraph(false);
            };

            /**
             * Called when a parent fragment is selected from fragment breadcrumbs.
             * Updates fragment breadcrumbs and gets new fragment data.
             * @param fragment  Fragment selected
             * @param index     Breadcrumb index
             */
            $scope.onParentFragmentSelected = function (fragment, index) {
                fragmentID = fragment.fragmentID;
                $scope.fragment = fragment;
                FragmentNavigationService.setLast(index);
                getDataForFragment();
            };

            /**
             * Called when a node in sankey directive is selected.
             * The node can represent either a fragment or a process.
             * @param newVal    The selected node
             */
            function onNodeSelectionChange(newVal) {
                if (newVal) {
                    var fragmentFlow = FragmentFlowService.get(newVal.nodeID);
                    $log.info("Clicked on node with weight = " + fragmentFlow.nodeWeight);
                    switch (newVal.nodeType) {
                        case "Process" :
                            $state.go("fragment-sankey.process-instance", {
                                    scenarioID: scenarioID,
                                    fragmentID: fragmentID,
                                    fragmentFlowID: fragmentFlow.fragmentFlowID,
                                    activity: $scope.fragment.activityLevel *
                                    fragmentFlow.nodeWeight
                                }
                            );
                            break;
                        case "Fragment" :
                            navigateSubFragment(fragmentFlow);
                            break;
                    }
                }
            }

            /**
             * Get flow table content
             * For each Sankey link provided, get
             * flow name, magnitude and unit associated with selected flow property.
             * If the link does have the selected flow property, display
             * magnitude and unit for the
             * reference flow property.
             *
             * @param {Array}  nodeLinks     Sankey links
             * @return {Array}  Data for display in table
             */
            function getFlowRows(nodeLinks) {
                var flowData = [];

                nodeLinks.forEach( function (l) {
                    if ("flowID" in l) {
                        var flow = FlowForFragmentService.get(l.flowID),
                            flowPropertyID = $scope.selectedFlowProperty["flowPropertyID"],
                            magnitude = l.magnitude,
                            unit = "";
                        if (magnitude) {
                            unit = $scope.selectedFlowProperty["referenceUnit"];
                        } else  {
                            var fp, ff;
                            ff = FragmentFlowService.get(l.nodeID);
                            flowPropertyID = flow["referenceFlowPropertyID"];
                            magnitude = getMagnitude(ff, flowPropertyID);
                            fp = FlowPropertyForFragmentService.get(flowPropertyID);
                            if (fp) {
                               unit = fp["referenceUnit"];
                            }
                        }
                        flowData.push({ name: flow.name, magnitude: magnitude, unit: unit });
                    }
                });
                return flowData;
            }

            function onMouseOverNode(node) {
                if (node) {
                    $scope.inputFlows = getFlowRows(node.targetLinks);
                    $scope.outputFlows = getFlowRows(node.sourceLinks);
                }
            }

            function getActiveScenarioID() {
                var activeID = ScenarioModelService.getActiveID();
                if (activeID) {
                    scenarioID = activeID;
                }
            }

            function getStateParams() {
                if ( $stateParams.hasOwnProperty("scenarioID") && $stateParams.scenarioID !== undefined) {
                    scenarioID = +$stateParams.scenarioID;
                }
                if ($stateParams.hasOwnProperty("fragmentID") && $stateParams.fragmentID !== undefined) {
                    fragmentID = +$stateParams.fragmentID;
                }
            }

            getActiveScenarioID();
            getStateParams();
            getData();
        }]);<|endoftext|>


<commit_before>import fm from '../../app';
import $ from 'jquery';
import L from 'leaflet';
import ng from 'angular';

fm.app.factory("fmMapMarkers", function($uibModal, fmUtils, $compile, $timeout) {
	return function(map) {
		var markersById = { };

		map.socket.on("marker", function(data) {
			if(map.socket.filterFunc(data))
				markersUi._addMarker(data);
		});

		map.socket.on("deleteMarker", function(data) {
			markersUi._deleteMarker(data);
		});

		map.socket.on("filter", function() {
			for(var i in map.socket.markers) {
				var show = map.socket.filterFunc(map.socket.markers[i]);
				if(markersById[i] && !show)
					markersUi._deleteMarker(map.socket.markers[i]);
				else if(!markersById[i] && show)
					markersUi._addMarker(map.socket.markers[i]);
			}
		});

		var markersUi = {
			_addMarker : function(marker) {
				if(!markersById[marker.id]) {
					markersById[marker.id] = L.marker([ 0, 0 ], { icon: fmUtils.createMarkerIcon(marker.colour, marker.size, marker.symbol)}).addTo(map.map)
						.bindPopup($("<div/>")[0], map.popupOptions)
						.on("popupopen", function(e) {
							markersUi._renderMarkerPopup(map.socket.markers[marker.id] || marker);
						})
						.on("popupclose", function(e) {
							ng.element(e.popup.getContent()).scope().$destroy();
						})
						.bindTooltip("", $.extend({}, map.tooltipOptions, { offset: [ 20, -15 ] }))
						.on("tooltipopen", function() {
							markersById[marker.id].setTooltipContent(fmUtils.quoteHtml(map.socket.markers[marker.id].name));
						});
				}

				markersById[marker.id]
					.setLatLng([ marker.lat, marker.lon ])
					.setIcon(fmUtils.createMarkerIcon(marker.colour, marker.size, marker.symbol));

				if(markersById[marker.id].isPopupOpen())
					markersUi._renderMarkerPopup(marker);
			},
			_deleteMarker : function(marker) {
				if(!markersById[marker.id])
					return;

				markersById[marker.id].removeFrom(map.map);
				delete markersById[marker.id];
			},
			_renderMarkerPopup: function(marker) {
				var scope = map.socket.$new();

				scope.marker = marker;

				scope.edit = function() {
					markersUi.editMarker(scope.marker);
				};

				scope.move = function() {
					markersUi.moveMarker(scope.marker);
				};

				scope['delete'] = function() {
					markersUi.deleteMarker(scope.marker);
				};

				var popup = markersById[marker.id].getPopup();
				var el = popup.getContent();
				$(el).html(require("./view-marker.html"));
				$compile(el)(scope);

				// Prevent popup close on button click
				$("button", el).click(function(e) {
					e.preventDefault();
				});

				$timeout(function() { $timeout(function() { // $compile only replaces variables on next digest
					popup.update();
				}); });
			},
			editMarker: function(marker) {
				var scope = map.socket.$new();

				var dialog = $uibModal.open({
					template: require("./edit-marker.html"),
					scope: scope,
					controller: "fmMapMarkerEditCtrl",
					size: "lg",
					resolve: {
						map: function() { return map; }
					}
				});

				var preserve = fmUtils.preserveObject(scope, "markers["+fmUtils.quoteJavaScript(marker.id)+"]", "marker", function() {
					dialog.dismiss();
				});

				dialog.result.then(preserve.leave.bind(preserve), preserve.revert.bind(preserve));
			},
			moveMarker: function(marker) {
				if(!markersById[marker.id])
					return;

				function _finish(save) {
					message.close();

					markersById[marker.id].dragging.disable();

					if(save) {
						var pos = markersById[marker.id].getLatLng();
						map.socket.emit("editMarker", { id: marker.id, lat: pos.lat, lon: pos.lng }).then(function() {
							markersById[marker.id].openPopup();
						}).catch(function(err) {
							map.messages.showMessage("danger", err);
						});
					}
				}

				map.map.closePopup();

				var message = map.messages.showMessage("info", "Drag the marker to reposition it.", [
					{ label: "Save", click: _finish.bind(null, true) },
					{ label: "Cancel", click: _finish}
				], null, _finish);

				markersById[marker.id].dragging.enable();
			},
			deleteMarker: function(marker) {
				map.socket.emit("deleteMarker", marker).catch(function(err) {
					map.messages.showMessage("danger", err);
				});
			},
			addMarker: function(type) {
				var message = map.messages.showMessage("info", "Please click on the map to add a marker.", [
					{ label: "Cancel", click: function() {
						message.close();
						listener.cancel();
					}}
				], null, function() { listener.cancel(); });

				map.map.closePopup();

				var listener = map.addClickListener(function(pos) {
					message.close();

					markersUi.createMarker(pos, type);
				});
			},
			createMarker: function(pos, type, properties) {
				map.socket.emit("addMarker", $.extend({ lon: pos.lon, lat: pos.lat, typeId: type.id }, properties)).then(function(marker) {
					markersUi._addMarker(marker);

					markersById[marker.id].openPopup();
					markersUi.editMarker(marker);
				}).catch(function(err) {
					map.messages.showMessage("danger", err);
				});
			}
		};

		return markersUi;
	};
});

fm.app.controller("fmMapMarkerEditCtrl", function($scope, map) {
	$scope.canControl = function(what) {
		return map.typesUi.canControl($scope.types[$scope.marker.typeId], what);
	};

	$scope.save = function() {
		$scope.error = null;
		map.socket.emit("editMarker", $scope.marker).then(function() {
			$scope.$close();
		}).catch(function(err) {
			$scope.error = err;
		});
	};

	$scope.$watchGroup([ "marker.colour", "marker.size", "marker.symbol" ], function() {
		map.markersUi._addMarker($scope.marker);
	});

});
<commit_msg>Move back marker when cancelling moving (fixes #48)<commit_after>import fm from '../../app';
import $ from 'jquery';
import L from 'leaflet';
import ng from 'angular';

fm.app.factory("fmMapMarkers", function($uibModal, fmUtils, $compile, $timeout) {
	return function(map) {
		var markersById = { };

		map.socket.on("marker", function(data) {
			if(map.socket.filterFunc(data))
				markersUi._addMarker(data);
		});

		map.socket.on("deleteMarker", function(data) {
			markersUi._deleteMarker(data);
		});

		map.socket.on("filter", function() {
			for(var i in map.socket.markers) {
				var show = map.socket.filterFunc(map.socket.markers[i]);
				if(markersById[i] && !show)
					markersUi._deleteMarker(map.socket.markers[i]);
				else if(!markersById[i] && show)
					markersUi._addMarker(map.socket.markers[i]);
			}
		});

		var markersUi = {
			_addMarker : function(marker) {
				if(!markersById[marker.id]) {
					markersById[marker.id] = L.marker([ 0, 0 ], { icon: fmUtils.createMarkerIcon(marker.colour, marker.size, marker.symbol)}).addTo(map.map)
						.bindPopup($("<div/>")[0], map.popupOptions)
						.on("popupopen", function(e) {
							markersUi._renderMarkerPopup(map.socket.markers[marker.id] || marker);
						})
						.on("popupclose", function(e) {
							ng.element(e.popup.getContent()).scope().$destroy();
						})
						.bindTooltip("", $.extend({}, map.tooltipOptions, { offset: [ 20, -15 ] }))
						.on("tooltipopen", function() {
							markersById[marker.id].setTooltipContent(fmUtils.quoteHtml(map.socket.markers[marker.id].name));
						});
				}

				markersById[marker.id]
					.setLatLng([ marker.lat, marker.lon ])
					.setIcon(fmUtils.createMarkerIcon(marker.colour, marker.size, marker.symbol));

				if(markersById[marker.id].isPopupOpen())
					markersUi._renderMarkerPopup(marker);
			},
			_deleteMarker : function(marker) {
				if(!markersById[marker.id])
					return;

				markersById[marker.id].removeFrom(map.map);
				delete markersById[marker.id];
			},
			_renderMarkerPopup: function(marker) {
				var scope = map.socket.$new();

				scope.marker = marker;

				scope.edit = function() {
					markersUi.editMarker(scope.marker);
				};

				scope.move = function() {
					markersUi.moveMarker(scope.marker);
				};

				scope['delete'] = function() {
					markersUi.deleteMarker(scope.marker);
				};

				var popup = markersById[marker.id].getPopup();
				var el = popup.getContent();
				$(el).html(require("./view-marker.html"));
				$compile(el)(scope);

				// Prevent popup close on button click
				$("button", el).click(function(e) {
					e.preventDefault();
				});

				$timeout(function() { $timeout(function() { // $compile only replaces variables on next digest
					popup.update();
				}); });
			},
			editMarker: function(marker) {
				var scope = map.socket.$new();

				var dialog = $uibModal.open({
					template: require("./edit-marker.html"),
					scope: scope,
					controller: "fmMapMarkerEditCtrl",
					size: "lg",
					resolve: {
						map: function() { return map; }
					}
				});

				var preserve = fmUtils.preserveObject(scope, "markers["+fmUtils.quoteJavaScript(marker.id)+"]", "marker", function() {
					dialog.dismiss();
				});

				dialog.result.then(preserve.leave.bind(preserve), preserve.revert.bind(preserve));
			},
			moveMarker: function(marker) {
				if(!markersById[marker.id])
					return;

				function _finish(save) {
					message.close();

					markersById[marker.id].dragging.disable();

					if(save) {
						var pos = markersById[marker.id].getLatLng();
						map.socket.emit("editMarker", { id: marker.id, lat: pos.lat, lon: pos.lng }).then(function() {
							markersById[marker.id].openPopup();
						}).catch(function(err) {
							map.messages.showMessage("danger", err);

							markersUi._addMarker(map.socket.markers[marker.id]);
						});
					} else {
						markersUi._addMarker(map.socket.markers[marker.id]);
					}
				}

				map.map.closePopup();

				var message = map.messages.showMessage("info", "Drag the marker to reposition it.", [
					{ label: "Save", click: _finish.bind(null, true) },
					{ label: "Cancel", click: _finish}
				], null, _finish);

				markersById[marker.id].dragging.enable();
			},
			deleteMarker: function(marker) {
				map.socket.emit("deleteMarker", marker).catch(function(err) {
					map.messages.showMessage("danger", err);
				});
			},
			addMarker: function(type) {
				var message = map.messages.showMessage("info", "Please click on the map to add a marker.", [
					{ label: "Cancel", click: function() {
						message.close();
						listener.cancel();
					}}
				], null, function() { listener.cancel(); });

				map.map.closePopup();

				var listener = map.addClickListener(function(pos) {
					message.close();

					markersUi.createMarker(pos, type);
				});
			},
			createMarker: function(pos, type, properties) {
				map.socket.emit("addMarker", $.extend({ lon: pos.lon, lat: pos.lat, typeId: type.id }, properties)).then(function(marker) {
					markersUi._addMarker(marker);

					markersById[marker.id].openPopup();
					markersUi.editMarker(marker);
				}).catch(function(err) {
					map.messages.showMessage("danger", err);
				});
			}
		};

		return markersUi;
	};
});

fm.app.controller("fmMapMarkerEditCtrl", function($scope, map) {
	$scope.canControl = function(what) {
		return map.typesUi.canControl($scope.types[$scope.marker.typeId], what);
	};

	$scope.save = function() {
		$scope.error = null;
		map.socket.emit("editMarker", $scope.marker).then(function() {
			$scope.$close();
		}).catch(function(err) {
			$scope.error = err;
		});
	};

	$scope.$watchGroup([ "marker.colour", "marker.size", "marker.symbol" ], function() {
		map.markersUi._addMarker($scope.marker);
	});

});
<|endoftext|>


<commit_before>const path = require('path')
const chalk = require('chalk')
const _ = require('lodash');
const AliOSS = require('ali-oss')
const Buffer = require('buffer').Buffer
const zlib = require('zlib')

const defaultConfig = {
  auth: {
    accessKeyId: '',
    accessKeySecret: '',
    bucket: '',
    region: ''
  },
  retry: 3, // 重试次数: number(>=0)
  existCheck: true, // true: 直接上传、false: 先检测,若已存在则不重新上传(不报错)
  // prefix 或者 ossBaseDir + project 二选一
  ossBaseDir: 'auto_upload_ci',
  project: '',
  prefix: '',
  exclude: /.*\.html$/,
  enableLog: false,
  ignoreError: false,
  removeMode: true,
  gzip: true,
  options: undefined
}

const red = chalk.red
const green = chalk.bold.green

module.exports = class WebpackAliOSSPlugin {

  // config = {} // 最终计算出来的配置参数
  // client = null // 阿里云 OSS 客户端
  // finalPrefix = '' // 最终计算出来的 prefix 路径

  constructor(cfg) {
    // 优化级顺序: 项目配置 > 环境变量 > 默认配置
    const envConfig = {
      auth: {
        accessKeyId: process.env.WEBPACK_ALIOSS_PLUGIN_ACCESS_KEY_ID,
        accessKeySecret: process.env.WEBPACK_ALIOSS_PLUGIN_ACCESS_KEY_SECRET,
        bucket: process.env.WEBPACK_ALIOSS_PLUGIN_BUCKET,
        region: process.env.WEBPACK_ALIOSS_PLUGIN_REGION,
      },
      enableLog: extraEnvBoolean(process.env.WEBPACK_ALIOSS_PLUGIN_ENABLE_LOG),
      ignoreError: extraEnvBoolean(process.env.WEBPACK_ALIOSS_PLUGIN_IGNORE_ERROR),
      removeMode: extraEnvBoolean(process.env.WEBPACK_ALIOSS_PLUGIN_REMOVE_MODE),
      ossBaseDir: process.env.WEBPACK_ALIOSS_PLUGIN_OSS_BASE_DIR,
      prefix: process.env.WEBPACK_ALIOSS_PLUGIN_PREFIX,
    }
    this.config = _.mergeWith(_.cloneDeep(defaultConfig), envConfig, cfg || {}, configMergeCustomizer)
    if (typeof this.config.retry !== 'number' || this.config.retry < 0) {
      this.config.retry = 0
    }
    this.calcPrefix()
    this.debug('默认配置:', defaultConfig)
    this.debug('环境变量配置:', envConfig)
    this.debug('项目配置:', cfg)
    this.debug('最终使用的配置:', this.config)
    // 初始化阿里云 OSS 客户端
    this.client = AliOSS(this.config.auth)
  }

  apply(compiler) {
    compiler.plugin('emit', (compilation, cb) => {
      const files = this.pickupAssetsFiles(compilation)
      log(`${green('\nOSS 上传开始......')}`)
      this.uploadFiles(files, compilation)
        .then(() => {
          log(`${green('OSS 上传完成\n')}`)
          cb()
        })
        .catch((err) => {
          log(`${red('OSS 上传出错')}::: ${red(err.code)}-${red(err.name)}: ${red(err.message)}`)
          this.config.ignoreError || compilation.errors.push(err)
          cb()
        })
    })
  }

  calcPrefix() {
    if (this.finalPrefix) return this.finalPrefix
    // 如果设置了 prefix, 则忽略 ossBaseDir 与 project
    if (this.config.prefix) {
      this.finalPrefix = this.config.prefix
    } else { // 使用 ossBaseDir 与 project
      // 如果 project 不存在, 则自动提取 package.json 中的 name 字段
      this.config.project = this.config.project || this.npmProjectName()
      if (!this.config.project) { // project 获取失败则直接使用 ossBaseDir 作为上传目录
        warn(`使用默认上传目录: ${this.config.ossBaseDir}`)
        this.finalPrefix = this.config.ossBaseDir
      } else {
        this.finalPrefix = `${this.config.ossBaseDir}/${this.config.project}`
      }

    }
    this.debug('使用的 OSS 目录:', this.finalPrefix)
    return this.finalPrefix
  }

  uploadFiles(files, compilation) {
    let i = 1
    return Promise.all(_.map(files, (file) => {
      file.$retryTime = 0
      const uploadName = `${this.calcPrefix()}/${file.name}`.replace('//', '/')
      // 先检测是否存在, 不存在则上传 TODO: 检测过程的日志打印
      if (this.config.existCheck !== true) {
        return this.uploadFile(file, i++, files, compilation, uploadName)
      } else {
        return new Promise((resolve, reject) => {
          this.client.list({
            prefix: uploadName,
            'max-keys': 1
          }).then(res => {
            if (res.objects && res.objects.length > 0) {
              const timeStr = getTimeStr(new Date(res.objects[0].lastModified))
              log(`${green('已存在,免上传')} (上传于 ${timeStr}) ${++i}/${files.length}: ${uploadName}`)
              this.config.removeMode && delete compilation.assets[file.name]
              resolve()
            } else {
              throw new Error('not exist & need upload')
            }
          }).catch(() => {
            this.uploadFile(file, i++, files, compilation, uploadName)
              .then((...rest) => resolve(rest))
              .catch(err => reject(err))
          })
        })
      }
    }))
  }
  uploadFile(file, idx, files, compilation, uploadName) {
    return new Promise((resolve, reject) => {
      const fileCount = files.length
      getFileContentBuffer(file, this.config.gzip).then((contentBuffer) => {
        const opt = this.getOptions(this.config.gzip)
        const self = this
        function _uploadAction() {
          file.$retryTime++
          log(`开始上传 ${idx}/${fileCount}: ${file.$retryTime > 1 ? '第' + (file.$retryTime - 1) + '次重试' : ''}`, uploadName)
          self.client.put(uploadName, contentBuffer, opt)
            .then(() => {
              log(`上传成功 ${idx}/${fileCount}: ${uploadName}`)
              self.config.removeMode && delete compilation.assets[file.name]
              resolve()
            })
            .catch(err => {
              if (file.$retryTime < self.config.retry + 1) {
                _uploadAction()
              } else {
                reject(err)
              }
            })
        }
        _uploadAction()
      }).catch(err => {
        reject(err)
      })
    })
  }
  getOptions(gzip) {
    const optValid = _.isPlainObject(this.config.options)
    if (gzip) {
      if (optValid) {
        if (!this.config.options.headers) this.config.options.headers = {}
        this.config.options.headers['Content-Encoding'] = 'gzip'
        return this.config.options
      } else {
        return {
          headers: { 'Content-Encoding': 'gzip' }
        }
      }
    } else {
      return optValid ? this.config.options : undefined
    }
  }

  // 从 compilation 对象中提取资源文件
  pickupAssetsFiles(compilation) {
    const matched = {}
    const keys = Object.keys(compilation.assets)
    for (let i = 0; i < keys.length; i++) {
      if (!this.config.exclude.test(keys[i])) {
        matched[keys[i]] = compilation.assets[keys[i]]
      }
    }
    return _.map(matched, (value, name) => ({
      name,
      path: value.existsAt,
      content: value.source()
    }))
  }

  // 提取所在项目 package.json 中的 name
  npmProjectName() {
    try {
      const pkg = require(path.resolve(process.env.PWD, 'package.json'))
      return pkg.name
    } catch (e) {
      return ''
    }
  }

  debug(...rest) {
    this.config.enableLog && log(...rest)
  }
}

function extraEnvBoolean(val) {
  if (val && val === 'true') {
    return true
  }
  if (val && val === 'false') {
    return false
  }
}

function getTimeStr(d) {
  return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()} ${d.getHours()}:${d.getMinutes()}`
}

function getFileContentBuffer(file, gzipVal) {
  const gzip = typeof gzipVal === 'number' || gzipVal === true ? true : false
  const opts = typeof gzipVal === 'number' ? { level: gzipVal } : {}
  if (!gzip) return Promise.resolve(Buffer.from(file.content))
  return new Promise((resolve, reject) => {
    zlib.gzip(Buffer.from(file.content), opts, (err, gzipBuffer) => {
      if (err) reject(err)
      resolve(gzipBuffer)
    })
  })
}

// 配置合并器
function configMergeCustomizer(objVal, srcVal) {
  if (_.isPlainObject(objVal) && _.isPlainObject(srcVal)) {
    return _.merge(objVal, srcVal)
  } else {
    return srcVal
  }
}

function log(...rest) {
  console.log(chalk.bgMagenta('[webpack-alioss-plugin]:'), ...rest) // eslint-disable-line
}
function warn(...rest) {
  console.warn(chalk.bgMagenta('[webpack-alioss-plugin]:'), ...rest) // eslint-disable-line
}
<commit_msg>fix(exist-check): 修复文件是否已存在的检测逻辑<commit_after>const path = require('path')
const chalk = require('chalk')
const _ = require('lodash');
const AliOSS = require('ali-oss')
const Buffer = require('buffer').Buffer
const zlib = require('zlib')

const defaultConfig = {
  auth: {
    accessKeyId: '',
    accessKeySecret: '',
    bucket: '',
    region: ''
  },
  retry: 3, // 重试次数: number(>=0)
  existCheck: true, // true: 直接上传、false: 先检测,若已存在则不重新上传(不报错)
  // prefix 或者 ossBaseDir + project 二选一
  ossBaseDir: 'auto_upload_ci',
  project: '',
  prefix: '',
  exclude: /.*\.html$/,
  enableLog: false,
  ignoreError: false,
  removeMode: true,
  gzip: true,
  options: undefined
}

const red = chalk.red
const green = chalk.bold.green

module.exports = class WebpackAliOSSPlugin {

  // config = {} // 最终计算出来的配置参数
  // client = null // 阿里云 OSS 客户端
  // finalPrefix = '' // 最终计算出来的 prefix 路径

  constructor(cfg) {
    // 优化级顺序: 项目配置 > 环境变量 > 默认配置
    const envConfig = {
      auth: {
        accessKeyId: process.env.WEBPACK_ALIOSS_PLUGIN_ACCESS_KEY_ID,
        accessKeySecret: process.env.WEBPACK_ALIOSS_PLUGIN_ACCESS_KEY_SECRET,
        bucket: process.env.WEBPACK_ALIOSS_PLUGIN_BUCKET,
        region: process.env.WEBPACK_ALIOSS_PLUGIN_REGION,
      },
      enableLog: extraEnvBoolean(process.env.WEBPACK_ALIOSS_PLUGIN_ENABLE_LOG),
      ignoreError: extraEnvBoolean(process.env.WEBPACK_ALIOSS_PLUGIN_IGNORE_ERROR),
      removeMode: extraEnvBoolean(process.env.WEBPACK_ALIOSS_PLUGIN_REMOVE_MODE),
      ossBaseDir: process.env.WEBPACK_ALIOSS_PLUGIN_OSS_BASE_DIR,
      prefix: process.env.WEBPACK_ALIOSS_PLUGIN_PREFIX,
    }
    this.config = _.mergeWith(_.cloneDeep(defaultConfig), envConfig, cfg || {}, configMergeCustomizer)
    if (typeof this.config.retry !== 'number' || this.config.retry < 0) {
      this.config.retry = 0
    }
    this.calcPrefix()
    this.debug('默认配置:', defaultConfig)
    this.debug('环境变量配置:', envConfig)
    this.debug('项目配置:', cfg)
    this.debug('最终使用的配置:', this.config)
    // 初始化阿里云 OSS 客户端
    this.client = AliOSS(this.config.auth)
  }

  apply(compiler) {
    compiler.plugin('emit', (compilation, cb) => {
      const files = this.pickupAssetsFiles(compilation)
      log(`${green('\nOSS 上传开始......')}`)
      this.uploadFiles(files, compilation)
        .then(() => {
          log(`${green('OSS 上传完成\n')}`)
          cb()
        })
        .catch((err) => {
          log(`${red('OSS 上传出错')}::: ${red(err.code)}-${red(err.name)}: ${red(err.message)}`)
          this.config.ignoreError || compilation.errors.push(err)
          cb()
        })
    })
  }

  calcPrefix() {
    if (this.finalPrefix) return this.finalPrefix
    // 如果设置了 prefix, 则忽略 ossBaseDir 与 project
    if (this.config.prefix) {
      this.finalPrefix = this.config.prefix
    } else { // 使用 ossBaseDir 与 project
      // 如果 project 不存在, 则自动提取 package.json 中的 name 字段
      this.config.project = this.config.project || this.npmProjectName()
      if (!this.config.project) { // project 获取失败则直接使用 ossBaseDir 作为上传目录
        warn(`使用默认上传目录: ${this.config.ossBaseDir}`)
        this.finalPrefix = this.config.ossBaseDir
      } else {
        this.finalPrefix = `${this.config.ossBaseDir}/${this.config.project}`
      }

    }
    this.debug('使用的 OSS 目录:', this.finalPrefix)
    return this.finalPrefix
  }

  uploadFiles(files, compilation) {
    let i = 1
    return Promise.all(_.map(files, (file) => {
      file.$retryTime = 0
      const uploadName = `${this.calcPrefix()}/${file.name}`.replace('//', '/')
      // 先检测是否存在, 不存在则上传 TODO: 检测过程的日志打印
      if (this.config.existCheck !== true) {
        return this.uploadFile(file, i++, files, compilation, uploadName)
      } else {
        return new Promise((resolve, reject) => {
          this.client.list({
            prefix: uploadName,
            'max-keys': 50
          }).then(res => {
            const arr = (res.objects || []).filter(item => item.name === uploadName)
            if (arr && arr.length > 0) {
              const timeStr = getTimeStr(new Date(res.objects[0].lastModified))
              log(`${green('已存在,免上传')} (上传于 ${timeStr}) ${++i}/${files.length}: ${uploadName}`)
              this.config.removeMode && delete compilation.assets[file.name]
              resolve()
            } else {
              throw new Error('not exist & need upload')
            }
          }).catch(() => {
            this.uploadFile(file, i++, files, compilation, uploadName)
              .then((...rest) => resolve(rest))
              .catch(err => reject(err))
          })
        })
      }
    }))
  }
  uploadFile(file, idx, files, compilation, uploadName) {
    return new Promise((resolve, reject) => {
      const fileCount = files.length
      getFileContentBuffer(file, this.config.gzip).then((contentBuffer) => {
        const opt = this.getOptions(this.config.gzip)
        const self = this
        function _uploadAction() {
          file.$retryTime++
          log(`开始上传 ${idx}/${fileCount}: ${file.$retryTime > 1 ? '第' + (file.$retryTime - 1) + '次重试' : ''}`, uploadName)
          self.client.put(uploadName, contentBuffer, opt)
            .then(() => {
              log(`上传成功 ${idx}/${fileCount}: ${uploadName}`)
              self.config.removeMode && delete compilation.assets[file.name]
              resolve()
            })
            .catch(err => {
              if (file.$retryTime < self.config.retry + 1) {
                _uploadAction()
              } else {
                reject(err)
              }
            })
        }
        _uploadAction()
      }).catch(err => {
        reject(err)
      })
    })
  }
  getOptions(gzip) {
    const optValid = _.isPlainObject(this.config.options)
    if (gzip) {
      if (optValid) {
        if (!this.config.options.headers) this.config.options.headers = {}
        this.config.options.headers['Content-Encoding'] = 'gzip'
        return this.config.options
      } else {
        return {
          headers: { 'Content-Encoding': 'gzip' }
        }
      }
    } else {
      return optValid ? this.config.options : undefined
    }
  }

  // 从 compilation 对象中提取资源文件
  pickupAssetsFiles(compilation) {
    const matched = {}
    const keys = Object.keys(compilation.assets)
    for (let i = 0; i < keys.length; i++) {
      if (!this.config.exclude.test(keys[i])) {
        matched[keys[i]] = compilation.assets[keys[i]]
      }
    }
    return _.map(matched, (value, name) => ({
      name,
      path: value.existsAt,
      content: value.source()
    }))
  }

  // 提取所在项目 package.json 中的 name
  npmProjectName() {
    try {
      const pkg = require(path.resolve(process.env.PWD, 'package.json'))
      return pkg.name
    } catch (e) {
      return ''
    }
  }

  debug(...rest) {
    this.config.enableLog && log(...rest)
  }
}

function extraEnvBoolean(val) {
  if (val && val === 'true') {
    return true
  }
  if (val && val === 'false') {
    return false
  }
}

function getTimeStr(d) {
  return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()} ${d.getHours()}:${d.getMinutes()}`
}

function getFileContentBuffer(file, gzipVal) {
  const gzip = typeof gzipVal === 'number' || gzipVal === true ? true : false
  const opts = typeof gzipVal === 'number' ? { level: gzipVal } : {}
  if (!gzip) return Promise.resolve(Buffer.from(file.content))
  return new Promise((resolve, reject) => {
    zlib.gzip(Buffer.from(file.content), opts, (err, gzipBuffer) => {
      if (err) reject(err)
      resolve(gzipBuffer)
    })
  })
}

// 配置合并器
function configMergeCustomizer(objVal, srcVal) {
  if (_.isPlainObject(objVal) && _.isPlainObject(srcVal)) {
    return _.merge(objVal, srcVal)
  } else {
    return srcVal
  }
}

function log(...rest) {
  console.log(chalk.bgMagenta('[webpack-alioss-plugin]:'), ...rest) // eslint-disable-line
}
function warn(...rest) {
  console.warn(chalk.bgMagenta('[webpack-alioss-plugin]:'), ...rest) // eslint-disable-line
}
<|endoftext|>


<commit_before>import styled, { css } from 'styled-components'
import { withState, lifecycle, compose } from 'recompose'
import Fighter from './Fighter'

import Share from '../components/Share'
const enhance = compose(
  withState('isMounted', 'setMounted', ({ instant }) => instant),
  lifecycle({
    componentWillMount() {},
    componentDidMount() {
      // 😖 This delay allows our animation to trigger on mount
      setTimeout(() => this.props.setMounted(true), 50)
      this.forceUpdate()
    }
  })
)

const Wrapper = styled.div`
  align-items: center;
  display: flex;
  flex-direction: column;
  justify-content: center;
  opacity: 0;
  padding: 0 2 rem;
  transition: 2s;

  ${props =>
    props.isMounted &&
    css`
      opacity: 1
  `};
`

const Image = styled.div`
  display: inline-block;
  margin-bottom: calc(1rem + 2vh);
  max-width: calc(2rem + 6vh + 1vw);
  width: 100%;

  @media (min-width: 420px) {
    max-width: calc(3rem + 6vh + 2vw);
  }
`

const Title = styled.div`
  font-family: 'Oswald', sans-serif;
  font-size: 2.5rem;
  font-weight: 600;
  letter-spacing: -0.025em;
  line-height: 1;
  max-width: calc(460px + 15vw);
  text-align: center;
  text-transform: uppercase;

  @media (min-width: 420px) {
    font-size: calc(1rem + 1.5vw + 1vh);
  }
`

const Meme = ({ asPath, activePage, meme, isMounted }) =>
  <Wrapper meme={meme} activePage={activePage} isMounted={isMounted}>
    <Image>
      <Fighter image={meme.raw.image} />
    </Image>
    <Title>
      “{meme.sentence}”
    </Title>
    <Share meme={meme} url={asPath} />
  </Wrapper>

export default enhance(Meme)
<commit_msg>rem fix<commit_after>import styled, { css } from 'styled-components'
import { withState, lifecycle, compose } from 'recompose'
import Fighter from './Fighter'

import Share from '../components/Share'
const enhance = compose(
  withState('isMounted', 'setMounted', ({ instant }) => instant),
  lifecycle({
    componentWillMount() {},
    componentDidMount() {
      // 😖 This delay allows our animation to trigger on mount
      setTimeout(() => this.props.setMounted(true), 50)
      this.forceUpdate()
    }
  })
)

const Wrapper = styled.div`
  align-items: center;
  display: flex;
  flex-direction: column;
  justify-content: center;
  opacity: 0;
  padding: 0 2rem;
  transition: 2s;

  ${props =>
    props.isMounted &&
    css`
      opacity: 1
  `};
`

const Image = styled.div`
  display: inline-block;
  margin-bottom: calc(1rem + 2vh);
  max-width: calc(2rem + 6vh + 1vw);
  width: 100%;

  @media (min-width: 420px) {
    max-width: calc(3rem + 6vh + 2vw);
  }
`

const Title = styled.div`
  font-family: 'Oswald', sans-serif;
  font-size: 2.5rem;
  font-weight: 600;
  letter-spacing: -0.025em;
  line-height: 1;
  max-width: calc(460px + 15vw);
  text-align: center;
  text-transform: uppercase;

  @media (min-width: 420px) {
    font-size: calc(1rem + 1.5vw + 1vh);
  }
`

const Meme = ({ asPath, activePage, meme, isMounted }) =>
  <Wrapper meme={meme} activePage={activePage} isMounted={isMounted}>
    <Image>
      <Fighter image={meme.raw.image} />
    </Image>
    <Title>
      “{meme.sentence}”
    </Title>
    <Share meme={meme} url={asPath} />
  </Wrapper>

export default enhance(Meme)
<|endoftext|>


<commit_before>/**
 * Поле редактирования имени файла.
 *
 * @author dew1983@mail.ru <Suvorov Andrey M.>
 */

Ext.define(
	'FBEditor.view.panel.filename.field.Field',
	{
		extend: 'Ext.form.field.Text',
		requires: [
			'FBEditor.view.panel.filename.field.FieldController'
		],
		id: 'panel-filename-field',
		xtype: 'panel-filename-field',
		controller: 'panel.filename.field',
		border: false,
		allowBlank: false,
		maxLength: 250,
		width: '100%',
		cls: 'panel-filename-field',
		listeners: {
			blur: 'onBlur',
			activate: 'onActivate',
			specialkey: 'onSpecialKey'
		}
	}
);<commit_msg>Ограничение длины вводимого текста в поле имени файла.<commit_after>/**
 * Поле редактирования имени файла.
 *
 * @author dew1983@mail.ru <Suvorov Andrey M.>
 */

Ext.define(
	'FBEditor.view.panel.filename.field.Field',
	{
		extend: 'Ext.form.field.Text',
		requires: [
			'FBEditor.view.panel.filename.field.FieldController'
		],
		id: 'panel-filename-field',
		xtype: 'panel-filename-field',
		controller: 'panel.filename.field',
		border: false,
		allowBlank: false,
		maxLength: 250,
		enforceMaxLength: true,
		width: '100%',
		cls: 'panel-filename-field',
		listeners: {
			blur: 'onBlur',
			activate: 'onActivate',
			specialkey: 'onSpecialKey'
		}
	}
);<|endoftext|>


<commit_before>if(!global._babelPolyfill) { require('babel-polyfill'); }

import Proto from 'uberproto';
import filter from 'feathers-query-filters';
import errors from 'feathers-errors';
import { nfcall, getSelect, multiOptions } from './utils';

// Create the service.
class Service {
  constructor(options) {
    if (!options) {
      throw new Error('NeDB options have to be provided');
    }

    if (!options.Model) {
      throw new Error('NeDB datastore `Model` needs to be provided');
    }

    this.Model = options.Model;
    this.id = options.id || '_id';
    this.paginate = options.paginate || {};
  }

  extend(obj) {
    return Proto.extend(obj, this);
  }

  _find(params, count, getFilter = filter) {
    // Start with finding all, and limit when necessary.
    let query = this.Model.find(params.query);
    let filters = getFilter(params.query|| {});

    // $select uses a specific find syntax, so it has to come first.
    if (filters.$select) {
      query = this.Model.find(params.query, getSelect(filters.$select));
    }

    // Handle $sort
    if (filters.$sort){
      query.sort(filters.$sort);
    }

    // Handle $limit
    if (filters.$limit){
      query.limit(filters.$limit);
    }

    // Handle $skip
    if (filters.$skip){
      query.skip(filters.$skip);
    }

    const runQuery = total => {
      return nfcall(query, 'exec').then(data => {
        return {
          total,
          limit: filters.$limit,
          skip: filters.$skip || 0,
          data
        };
      });
    };

    if(count) {
      return nfcall(this.Model, 'count', params.query).then(runQuery);
    }

    return runQuery();
  }

  find(params) {
    const paginate = (params && typeof params.paginate !== 'undefined') ?
      params.paginate : this.paginate;
    const result = this._find(params, !!paginate.default,
      query => filter(query, paginate));

    if(!paginate.default) {
      return result.then(page => page.data);
    }

    return result;
  }

  _get(id) {
    return nfcall(this.Model, 'findOne', { [this.id]: id }).then(doc => {
      if(!doc) {
        throw new errors.NotFound(`No record found for id '${id}'`);
      }

      return doc;
    });
  }

  get(id, params) {
    return this._get(id, params);
  }

  _findOrGet(id, params) {
    if(id === null) {
      return this._find(params).then(page => page.data);
    }

    return this._get(id, params);
  }

  create(data) {
    return nfcall(this.Model, 'insert', data);
  }

  patch(id, data, params) {
    let { query, options } = multiOptions(id, this.id, params);

    // We can not update the id
    delete data[this.id];

    // Run the query
    return nfcall(this.Model, 'update', query, { $set: data }, options)
      .then(() => this._findOrGet(id, params));
  }

  update(id, data, params) {
    if(Array.isArray(data) || id === null) {
      return Promise.reject('Not replacing multiple records. Did you mean `patch`?');
    }

    let { query, options } = multiOptions(id, this.id, params);

    // We can not update the id
    delete data[this.id];

    return nfcall(this.Model, 'update', query, data, options)
      .then(() => this._findOrGet(id));
  }

  remove(id, params) {
    let { query, options } = multiOptions(id, this.id, params);

    return this._findOrGet(id, params).then(items =>
      nfcall(this.Model, 'remove', query, options)
        .then(() => items)
    );
  }
}

export default function init(options) {
  return new Service(options);
}

init.Service = Service;
<commit_msg>fix errors in update and patch with custom id (#20)<commit_after>if(!global._babelPolyfill) { require('babel-polyfill'); }

import Proto from 'uberproto';
import filter from 'feathers-query-filters';
import errors from 'feathers-errors';
import { nfcall, getSelect, multiOptions } from './utils';

// Create the service.
class Service {
  constructor(options) {
    if (!options) {
      throw new Error('NeDB options have to be provided');
    }

    if (!options.Model) {
      throw new Error('NeDB datastore `Model` needs to be provided');
    }

    this.Model = options.Model;
    this.id = options.id || '_id';
    this.paginate = options.paginate || {};
  }

  extend(obj) {
    return Proto.extend(obj, this);
  }

  _find(params, count, getFilter = filter) {
    // Start with finding all, and limit when necessary.
    let query = this.Model.find(params.query);
    let filters = getFilter(params.query|| {});

    // $select uses a specific find syntax, so it has to come first.
    if (filters.$select) {
      query = this.Model.find(params.query, getSelect(filters.$select));
    }

    // Handle $sort
    if (filters.$sort){
      query.sort(filters.$sort);
    }

    // Handle $limit
    if (filters.$limit){
      query.limit(filters.$limit);
    }

    // Handle $skip
    if (filters.$skip){
      query.skip(filters.$skip);
    }

    const runQuery = total => {
      return nfcall(query, 'exec').then(data => {
        return {
          total,
          limit: filters.$limit,
          skip: filters.$skip || 0,
          data
        };
      });
    };

    if(count) {
      return nfcall(this.Model, 'count', params.query).then(runQuery);
    }

    return runQuery();
  }

  find(params) {
    const paginate = (params && typeof params.paginate !== 'undefined') ?
      params.paginate : this.paginate;
    const result = this._find(params, !!paginate.default,
      query => filter(query, paginate));

    if(!paginate.default) {
      return result.then(page => page.data);
    }

    return result;
  }

  _get(id) {
    return nfcall(this.Model, 'findOne', { [this.id]: id }).then(doc => {
      if(!doc) {
        throw new errors.NotFound(`No record found for id '${id}'`);
      }

      return doc;
    });
  }

  get(id, params) {
    return this._get(id, params);
  }

  _findOrGet(id, params) {
    if(id === null) {
      return this._find(params).then(page => page.data);
    }

    return this._get(id, params);
  }

  create(data) {
    return nfcall(this.Model, 'insert', data);
  }

  patch(id, data, params) {
    let { query, options } = multiOptions(id, this.id, params);

    // We can not update the id
    delete data[this.id];
    delete data._id;

    // Run the query
    return nfcall(this.Model, 'update', query, { $set: data }, options)
      .then(() => this._findOrGet(id, params));
  }

  update(id, data, params) {
    if(Array.isArray(data) || id === null) {
      return Promise.reject('Not replacing multiple records. Did you mean `patch`?');
    }

    let { query, options } = multiOptions(id, this.id, params);

    // We can not update the id
    data[this.id] = id;
    delete data._id;

    return nfcall(this.Model, 'update', query, data, options)
      .then(() => this._findOrGet(id));
  }

  remove(id, params) {
    let { query, options } = multiOptions(id, this.id, params);

    return this._findOrGet(id, params).then(items =>
      nfcall(this.Model, 'remove', query, options)
        .then(() => items)
    );
  }
}

export default function init(options) {
  return new Service(options);
}

init.Service = Service;
<|endoftext|>


<commit_before>const path                    = require('path')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
const webpack                 = require('webpack')
const TerserJSPlugin          = require('terser-webpack-plugin')
const MiniCssExtractPlugin    = require('mini-css-extract-plugin')
const CssMinimizerPlugin      = require('css-minimizer-webpack-plugin')
const HtmlWebpackPlugin       = require('html-webpack-plugin')

module.exports = function ({ runtime }) {
  const assetDirs = [
    `${runtime.assetsSourceDir}`,
  ].concat((runtime.extraAssetsSourceDirs || []))

  const moduleDirs = [
    runtime.assetsSourceDir,
    'node_modules',
    path.join(runtime.npmRoot, 'node_modules'),
    path.join(runtime.lanyonDir, 'node_modules'),
  ].concat(runtime.extraAssetsSourceDirs || [])

  const browsers = runtime.browsers || ['> 1%', 'ie 10', 'ie 8', 'safari 4']

  const webpackRules = () => {
    const rules = []

    rules.push({
      test: /\.woff2?(\?v=\d+\.\d+\.\d+)?$/,
      type: 'asset',
    })

    rules.push({
      test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
      type: 'asset',
    })

    rules.push({
      test: /\.(png|webp|gif|jpe?g|ttf(\?v=\d+\.\d+\.\d+)?)$/,
      type: 'asset',
    })

    rules.push({
      test: /\.(eot|cur)(\?v=\d+\.\d+\.\d+)?$/,
      type: 'asset/resource',
    })

    rules.push({
      test: /\.worker\.js$/,
      use : [
        'worker-loader',
      ],
    })

    rules.push({
      test: /([\\/]bootstrap-sass[\\/]assets[\\/]javascripts[\\/]|[\\/]jquery\..*\.js$)/,
      use : [
        {
          loader : 'imports-loader',
          options: {
            imports: [
              'default jquery $',
            ],
            // Disabled for Webpack5
            // this: '>window',
          },
        },
      ],
    })

    rules.push({
      test: /\.(sa|sc|c)ss$/,
      use : [
        MiniCssExtractPlugin.loader,
        {
          loader : 'cache-loader',
          options: {
            cacheDirectory: `${runtime.cacheDir}/cache-loader`,
          },
        },
        'css-loader',
        {
          loader : 'postcss-loader',
          options: {
            postcssOptions: {
              sourceMap: true,
              ident    : 'postcss',
              plugins  : [
                ['postcss-preset-env', { browsers }],
              ],
            },
          },
        },
        {
          loader : 'sass-loader',
          options: {
            sassOptions: {
              // We can't do anything about deprecations in dependency code
              quietDeps: true,
            },
          },
        },
      ],
    })

    rules.push({
      test   : /\.tsx?$/,
      use    : 'ts-loader',
      exclude: /node_modules/,
    })

    rules.push({
      test   : /\.(js|jsx)$/,
      include: assetDirs,
      exclude: [
        /[\\/](node_modules|js-untouched)[\\/]/,
      ],
      use: [
        'thread-loader',
        {
          loader : 'babel-loader',
          options: {
            babelrc         : false,
            cacheCompression: false,
            cacheDirectory  : `${runtime.cacheDir}/babel-loader`,
            presets         : [
              [require.resolve('@babel/preset-env'), {
                targets: { browsers },
              }],
              require.resolve('@babel/preset-react'),
            ],
            plugins: [
              [require.resolve('@babel/plugin-proposal-decorators'), { legacy: true }],
              require.resolve('@babel/plugin-proposal-class-properties'),
              require.resolve('react-hot-loader/babel'),
              require.resolve('nanohtml'),
            ],
          },
        },
      ],
    })

    return rules
  }

  const webpackPlugins = () => {
    const plugins = []

    const defines = {
      'process.env.LANYON_ENV': JSON.stringify(runtime.lanyonEnv),
      'process.env.NODE_ENV'  : JSON.stringify(process.env.NODE_ENV),
      'process.env.ENDPOINT'  : JSON.stringify(process.env.ENDPOINT),
    }

    if (runtime.customEnv) {
      for (const [key, value] of Object.entries(runtime.customEnv)) {
        defines[`process.env.${key}`] = JSON.stringify(value)
      }
    }

    plugins.push(new webpack.DefinePlugin(defines))

    runtime.entries.forEach(entry => {
      plugins.push(new HtmlWebpackPlugin({
        inject         : false,
        cache          : true,
        scriptLoading  : 'blocking', // worth an experiment: 'defer'
        filename       : `${runtime.projectDir}/_includes/_generated_assets/${entry}-${runtime.lanyonEnv}-head.html`,
        chunks         : [entry],
        templateContent: runtime.headAssetTemplate ? runtime.headAssetTemplate  : ({ htmlWebpackPlugin }) => `${htmlWebpackPlugin.tags.headTags}`,
      }))
      plugins.push(new HtmlWebpackPlugin({
        inject         : false,
        cache          : true,
        scriptLoading  : 'blocking', // worth an experiment: 'defer'
        filename       : `${runtime.projectDir}/_includes/_generated_assets/${entry}-${runtime.lanyonEnv}-body.html`,
        chunks         : [entry],
        templateContent: runtime.bodyAssetTemplate ? runtime.bodyAssetTemplate : ({ htmlWebpackPlugin }) => `${htmlWebpackPlugin.tags.bodyTags}`,
      }))
    })
    // plugins.push({
    //   apply: (compiler) => {
    //     compiler.hooks.afterEmit.tap('AfterEmitPlugin', async (compilation) => {
    //       const files = await globby(`${runtime.cacheDir}/_generated_assets/*`)
    //       const targetDir = `${runtime.projectDir}/_includes/_generated_assets/`

    //       console.log({ files, targetDir })
    //     })
    //   },
    // })
    plugins.push(new MiniCssExtractPlugin({
      // Options similar to the same options in webpackOptions.output
      // both options are optional
      filename     : runtime.isDev ? `[name].css` : `[name].[contenthash].css`,
      chunkFilename: runtime.isDev ? `[name].css` : `[name].[contenthash].[chunkhash].chunk.css`,
      ignoreOrder  : true, // <-- add this to avoid: "Order in extracted chunk undefined" ¯\_(ツ)_/¯ https://github.com/redbadger/website-honestly/issues/128
    }))

    if (runtime.isDev) {
      plugins.push(new webpack.HotModuleReplacementPlugin())
    }

    if (runtime.analyze) {
      plugins.push(new BundleAnalyzerPlugin({
        analyzerMode: 'static',
        logLevel    : 'info',
        openAnalyzer: true,
      }))
    }

    return plugins
  }

  const webpackCfg = {
    mode        : runtime.isDev ? 'development' : 'production',
    bail        : true,
    module      : { rules: webpackRules() },
    plugins     : webpackPlugins(),
    // Disabled for Webpack5
    node        : false,
    recordsPath : runtime.recordsPath,
    target      : 'web',
    optimization: {
      minimize   : !runtime.isDev,
      minimizer  : !runtime.isDev ? [new TerserJSPlugin({}), new CssMinimizerPlugin({})] : [],
      splitChunks: !runtime.isDev
        ? {
          chunks: 'all',
        }
        : false,
    },
    output: {
      publicPath   : runtime.publicPath,
      path         : runtime.assetsBuildDir,
      filename     : runtime.isDev ? `[name].js` : `[name].[contenthash].js`,
      chunkFilename: runtime.isDev ? `[name].js` : `[name].[contenthash].[chunkhash].chunk.js`,
    },
    devtool: (function dynamicDevtool () {
      // https://webpack.js.org/guides/build-performance/#devtool
      if (runtime.isDev) {
        return 'eval-cheap-module-source-map'
      }

      return 'source-map'
    }()),
    resolveLoader: {
      modules: [
        path.join(runtime.lanyonDir, 'node_modules'),
        path.join(runtime.npmRoot, 'node_modules'),
        path.join(runtime.projectDir, 'node_modules'),
      ],
    },
    resolve: {
      modules         : moduleDirs,
      descriptionFiles: ['package.json'],
      mainFields      : ['browser', 'main'],
      mainFiles       : ['index'],
      aliasFields     : ['browser'],
      extensions      : ['.tsx', '.ts', '.js'],
      enforceExtension: false,
      fallback        : {
        stream: require.resolve('stream-browserify'),
      },
      // Disabled for Webpack5
      // enforceModuleExtension: false,
      alias: runtime.alias,
    },
    entry: (function dynamicEntries () {
      const entries = {}

      runtime.entries.forEach(entry => {
        entries[entry] = []
        if (runtime.isDev) {
          // Push HMR to all entrypoints
          entries[entry].push('webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000&reload=true&overlay=true')
        }

        // Add .js if we do not have a valid extension (js/ts) already for the entrypoint
        const entryWithExt = ['.js', '.ts'].includes(path.extname(entry)) ? entry : `${entry}.js`

        entries[entry].push(path.join(runtime.assetsSourceDir, entryWithExt))
      })

      return entries
    }()),
  }

  return webpackCfg
}
<commit_msg>Update config.webpack.js<commit_after>const path                    = require('path')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
const webpack                 = require('webpack')
const TerserJSPlugin          = require('terser-webpack-plugin')
const MiniCssExtractPlugin    = require('mini-css-extract-plugin')
const CssMinimizerPlugin      = require('css-minimizer-webpack-plugin')
const HtmlWebpackPlugin       = require('html-webpack-plugin')

module.exports = function ({ runtime }) {
  const assetDirs = [
    `${runtime.assetsSourceDir}`,
  ].concat((runtime.extraAssetsSourceDirs || []))

  const moduleDirs = [
    runtime.assetsSourceDir,
    'node_modules',
    path.join(runtime.npmRoot, 'node_modules'),
    path.join(runtime.lanyonDir, 'node_modules'),
  ].concat(runtime.extraAssetsSourceDirs || [])

  const browsers = runtime.browsers || ['> 1%', 'ie 10', 'ie 8', 'safari 4']

  const webpackRules = () => {
    const rules = []

    rules.push({
      test: /\.woff2?(\?v=\d+\.\d+\.\d+)?$/,
      type: 'asset',
    })

    rules.push({
      test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
      type: 'asset',
    })

    rules.push({
      test: /\.(png|webp|gif|jpe?g|ttf(\?v=\d+\.\d+\.\d+)?)$/,
      type: 'asset',
    })

    rules.push({
      test: /\.(eot|cur)(\?v=\d+\.\d+\.\d+)?$/,
      type: 'asset/resource',
    })

    rules.push({
      test: /\.worker\.js$/,
      use : [
        'worker-loader',
      ],
    })

    rules.push({
      test: /([\\/]bootstrap-sass[\\/]assets[\\/]javascripts[\\/]|[\\/]jquery\..*\.js$)/,
      use : [
        {
          loader : 'imports-loader',
          options: {
            imports: [
              'default jquery $',
            ],
            // Disabled for Webpack5
            // this: '>window',
          },
        },
      ],
    })

    rules.push({
      test: /\.(sa|sc|c)ss$/,
      use : [
        MiniCssExtractPlugin.loader,
        {
          loader : 'cache-loader',
          options: {
            cacheDirectory: `${runtime.cacheDir}/cache-loader`,
          },
        },
        'css-loader',
        {
          loader : 'postcss-loader',
          options: {
            postcssOptions: {
              sourceMap: true,
              ident    : 'postcss',
              plugins  : [
                ['postcss-preset-env', { browsers }],
              ],
            },
          },
        },
        {
          loader : 'sass-loader',
          options: {
            sassOptions: {
              // We can't do anything about deprecations in dependency code
              quietDeps: true,
            },
          },
        },
      ],
    })

    rules.push({
      test   : /\.tsx?$/,
      use    : 'ts-loader',
      exclude: /node_modules/,
    })

    rules.push({
      test   : /\.(js|jsx)$/,
      include: assetDirs,
      exclude: [
        /[\\/](node_modules|js-untouched)[\\/]/,
      ],
      use: [
        'thread-loader',
        {
          loader : 'babel-loader',
          options: {
            babelrc         : false,
            cacheCompression: false,
            cacheDirectory  : `${runtime.cacheDir}/babel-loader`,
            presets         : [
              [require.resolve('@babel/preset-env'), {
                targets: { browsers },
              }],
              require.resolve('@babel/preset-react'),
            ],
            plugins: [
              [require.resolve('@babel/plugin-proposal-decorators'), { legacy: true }],
              require.resolve('@babel/plugin-proposal-class-properties'),
              require.resolve('react-hot-loader/babel'),
              require.resolve('nanohtml'),
            ],
          },
        },
      ],
    })

    return rules
  }

  const webpackPlugins = () => {
    const plugins = []

    const defines = {
      'process.env.LANYON_ENV': JSON.stringify(runtime.lanyonEnv),
      'process.env.NODE_ENV'  : JSON.stringify(process.env.NODE_ENV),
      'process.env.ENDPOINT'  : JSON.stringify(process.env.ENDPOINT),
    }

    if (runtime.customEnv) {
      for (const [key, value] of Object.entries(runtime.customEnv)) {
        defines[`process.env.${key}`] = JSON.stringify(value)
      }
    }

    plugins.push(new webpack.DefinePlugin(defines))

    runtime.entries.forEach(entry => {
      const withoutExtension = entry.replace(/\.(t|j)sx?$/, '')

      plugins.push(new HtmlWebpackPlugin({
        inject         : false,
        cache          : true,
        scriptLoading  : 'blocking', // worth an experiment: 'defer'
        filename       : `${runtime.projectDir}/_includes/_generated_assets/${withoutExtension}-${runtime.lanyonEnv}-head.html`,
        chunks         : [entry],
        templateContent: runtime.headAssetTemplate ? runtime.headAssetTemplate  : ({ htmlWebpackPlugin }) => `${htmlWebpackPlugin.tags.headTags}`,
      }))
      plugins.push(new HtmlWebpackPlugin({
        inject         : false,
        cache          : true,
        scriptLoading  : 'blocking', // worth an experiment: 'defer'
        filename       : `${runtime.projectDir}/_includes/_generated_assets/${withoutExtension}-${runtime.lanyonEnv}-body.html`,
        chunks         : [entry],
        templateContent: runtime.bodyAssetTemplate ? runtime.bodyAssetTemplate : ({ htmlWebpackPlugin }) => `${htmlWebpackPlugin.tags.bodyTags}`,
      }))
    })
    // plugins.push({
    //   apply: (compiler) => {
    //     compiler.hooks.afterEmit.tap('AfterEmitPlugin', async (compilation) => {
    //       const files = await globby(`${runtime.cacheDir}/_generated_assets/*`)
    //       const targetDir = `${runtime.projectDir}/_includes/_generated_assets/`

    //       console.log({ files, targetDir })
    //     })
    //   },
    // })
    plugins.push(new MiniCssExtractPlugin({
      // Options similar to the same options in webpackOptions.output
      // both options are optional
      filename     : runtime.isDev ? `[name].css` : `[name].[contenthash].css`,
      chunkFilename: runtime.isDev ? `[name].css` : `[name].[contenthash].[chunkhash].chunk.css`,
      ignoreOrder  : true, // <-- add this to avoid: "Order in extracted chunk undefined" ¯\_(ツ)_/¯ https://github.com/redbadger/website-honestly/issues/128
    }))

    if (runtime.isDev) {
      plugins.push(new webpack.HotModuleReplacementPlugin())
    }

    if (runtime.analyze) {
      plugins.push(new BundleAnalyzerPlugin({
        analyzerMode: 'static',
        logLevel    : 'info',
        openAnalyzer: true,
      }))
    }

    return plugins
  }

  const webpackCfg = {
    mode        : runtime.isDev ? 'development' : 'production',
    bail        : true,
    module      : { rules: webpackRules() },
    plugins     : webpackPlugins(),
    // Disabled for Webpack5
    node        : false,
    recordsPath : runtime.recordsPath,
    target      : 'web',
    optimization: {
      minimize   : !runtime.isDev,
      minimizer  : !runtime.isDev ? [new TerserJSPlugin({}), new CssMinimizerPlugin({})] : [],
      splitChunks: !runtime.isDev
        ? {
          chunks: 'all',
        }
        : false,
    },
    output: {
      publicPath   : runtime.publicPath,
      path         : runtime.assetsBuildDir,
      filename     : runtime.isDev ? `[name].js` : `[name].[contenthash].js`,
      chunkFilename: runtime.isDev ? `[name].js` : `[name].[contenthash].[chunkhash].chunk.js`,
    },
    devtool: (function dynamicDevtool () {
      // https://webpack.js.org/guides/build-performance/#devtool
      if (runtime.isDev) {
        return 'eval-cheap-module-source-map'
      }

      return 'source-map'
    }()),
    resolveLoader: {
      modules: [
        path.join(runtime.lanyonDir, 'node_modules'),
        path.join(runtime.npmRoot, 'node_modules'),
        path.join(runtime.projectDir, 'node_modules'),
      ],
    },
    resolve: {
      modules         : moduleDirs,
      descriptionFiles: ['package.json'],
      mainFields      : ['browser', 'main'],
      mainFiles       : ['index'],
      aliasFields     : ['browser'],
      extensions      : ['.tsx', '.ts', '.js'],
      enforceExtension: false,
      fallback        : {
        stream: require.resolve('stream-browserify'),
      },
      // Disabled for Webpack5
      // enforceModuleExtension: false,
      alias: runtime.alias,
    },
    entry: (function dynamicEntries () {
      const entries = {}

      runtime.entries.forEach(entry => {
        entries[entry] = []
        if (runtime.isDev) {
          // Push HMR to all entrypoints
          entries[entry].push('webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000&reload=true&overlay=true')
        }

        // Add .js if we do not have a valid extension (js/ts) already for the entrypoint
        const entryWithExt = ['.js', '.ts'].includes(path.extname(entry)) ? entry : `${entry}.js`

        entries[entry].push(path.join(runtime.assetsSourceDir, entryWithExt))
      })

      return entries
    }()),
  }

  return webpackCfg
}
<|endoftext|>