UNPKG

7.57 kBJavaScriptView Raw
1/*!
2Kettle Utilities.
3
4Copyright 2012-2013 OCAD University
5Copyright 2012 Antranig Basman
6
7Licensed under the New BSD license. You may not use this file except in
8compliance with this License.
9
10You may obtain a copy of the License at
11https://github.com/fluid-project/kettle/blob/master/LICENSE.txt
12*/
13
14"use strict";
15
16var fluid = require("infusion"),
17 os = require("os"),
18 fs = require("fs"),
19 path = require("path");
20
21var kettle = fluid.registerNamespace("kettle"),
22 $ = fluid.registerNamespace("jQuery");
23
24fluid.extend = $.extend; // passthrough definitions, compatible with upcoming Infusion
25fluid.trim = $.trim;
26
27// Debugging definition - node.js's default is only 10!
28fluid.Error.stackTraceLimit = 100;
29Error.stackTraceLimit = 100;
30
31kettle.requestUncaughtExceptionHandler = function (err) {
32 var request = kettle.getCurrentRequest();
33 // If there is no active request
34 if (request) {
35 request.events.onError.fire({
36 isError: true,
37 message: err.message
38 });
39 }
40};
41
42fluid.onUncaughtException.addListener(kettle.requestUncaughtExceptionHandler, "abortKettleRequest",
43 "before:fail");
44
45
46// In case of a fluid.fail - abort any current request, and then throw an exception - but give the current
47// request a chance to return a better-contextualised error message first
48kettle.failureHandler = function (args, activity) {
49 var request = kettle.getCurrentRequest();
50 if (request) {
51 fluid.invokeLater(function () {
52 if (!request.handlerPromise.disposition) {
53 request.handlerPromise.reject({
54 isError: true,
55 message: args[0]
56 });
57 }
58 });
59 }
60 fluid.builtinFail(args, activity);
61};
62
63fluid.failureEvent.addListener(kettle.failureHandler, "fail");
64
65// There seems to be no other way to determine whether signals are supported
66// than direct OS detection. Signals are currently completely unsupported on
67// Windows - https://github.com/joyent/node/issues/1553
68// The purpose of this code is to avoid hung or detached processes if node
69// is "killed" with CTRL-C etc.
70/* istanbul ignore next */
71if (os.type().indexOf("Windows") === -1) {
72 process.on("SIGTERM", function handler() {
73 process.exit(0);
74 });
75}
76
77/** Upgrades a promise rejection payload (or Error) by suffixing an additional "while" reason into its "message" field
78 * @param {Object|Error} originError - A rejection payload. This should (at least) have the member `isError: true` set, as well as a String `message` holding a rejection reason.
79 * @param {String} whileMsg - A message describing the activity which led to this error
80 * @return {Object} The rejected payload formed by shallow cloning the supplied argument (if it is not an `Error`) and suffixing its `message` member
81 */
82kettle.upgradeError = function (originError, whileMsg) {
83 var error = originError instanceof Error ? originError : fluid.extend({}, originError);
84 error.message = originError.message + whileMsg;
85 return error;
86};
87
88/** Clones an Error object into a JSON equivalent so that it can be checked in a test fixture using deep equality
89 * @param {Object|Error} originError - The error or rejection payload to be cloned
90 * @return {Object} The error represented as a plain object
91 */
92kettle.cloneError = function (originError) {
93 var togo = fluid.extend({}, originError);
94 togo.message = originError.message;
95 return togo;
96};
97
98/** After express 4.15.0 of 2017-03-01 error messages are packaged as HTML readable
99 * in this stereotypical form.
100 * @param received {String} The body of an HTTP response from express which has
101 * completed with an error
102 * @return {String} The inner error encoded within the HTML body of the response,
103 * if it could be decoded, or else the original value of `received`.
104 */
105
106kettle.extractHtmlError = function (received) {
107 var matches = /<pre>(.*)<\/pre>/gm.exec(received);
108 return matches ? matches[1] : received;
109};
110
111/** Returns a synchronously resolving promise for the contents of the supplied filename
112 * @param {String} fileName - The name of the file to be loaded (with `fs.readFileSync`)
113 * @param {String} message - A string describing the activity requiring the file, to appear in any rejection message
114 * @return {Promise} A promise for the file's contents decoded as UTF-8
115 */
116kettle.syncFilePromise = function (fileName, message) {
117 var togo = fluid.promise();
118 try {
119 var string = fs.readFileSync(fileName, "utf8");
120 togo.resolve(string);
121 } catch (e) {
122 togo.reject({
123 message: "Error " + message + ": " + e,
124 error: e
125 });
126 }
127 togo.accumulateRejectionReason = function (originError) {
128 return kettle.upgradeError(originError, " while " + message);
129 };
130 return togo;
131};
132
133/** @param {String[]} fileNames - An array of filenames to be tested
134 * @return {String} The first member of the array which exists, or `null` if none do
135 */
136kettle.firstExistingFile = function (fileNames) {
137 return fluid.find_if(fileNames, function (fileName) {
138 try {
139 return fs.statSync(fileName).isFile();
140 } catch (e) {
141 return false;
142 }
143 }, null);
144};
145
146fluid.registerNamespace("kettle.JSON");
147
148/**
149 * A function parsing a string into an Object form or failure
150 *
151 * @callback ConfigParser
152 * @param {String} The content to be parsed
153 * @return {Promise} The parsed form of the content, or a rejection
154 */
155
156/**
157 * Parses a config file by chosing the appropriate decoder based on the extension of the file
158 * Currently json and json5 are supported
159 * @param {String} fileName - The name of the file required to be loaded
160 * @return {ConfigParser} The parser to use (defaulting to a JSON parser as implemented by `kettle.dataSource.parseJSON`)
161 */
162kettle.JSON.getSuitableParser = function (fileName) {
163 return (path.extname(fileName).toLowerCase() === ".json5") ?
164 kettle.dataSource.parseJSON5 : kettle.dataSource.parseJSON;
165};
166
167kettle.JSON.readFileSync = function (fileName, message) {
168 message = message || "reading file " + fileName;
169 var sequence = [{
170 listener: function () {
171 return kettle.syncFilePromise(fileName, message);
172 }
173 }, {
174 listener: kettle.JSON.getSuitableParser(fileName)
175 }];
176 var sequencer = fluid.promise.makeTransformer(sequence);
177 // We previously bound to the unstable API fluid.promise.makeTransformer which originally started the sequence
178 // automatically, and now take the compatibility hit now it doesn't following FLUID-6445
179 if (sequencer.resolvedSources.length === 0) {
180 fluid.promise.resumeSequence(sequencer);
181 }
182 return sequencer.promise;
183};
184
185// Direct replacement for JSON.parse which instead throws a helpful diagnostic (currently using jsonlint)
186kettle.JSON.parse = function (string) {
187 if (typeof(string) !== "string") {
188 fluid.fail("kettle.JSON.parse called on non-string object ", string);
189 }
190 var togo;
191 kettle.dataSource.parseJSON(string).then(function (parsed) {
192 togo = parsed;
193 }, function (err) {
194 throw err;
195 });
196 return togo;
197};
198
199// Distribute commonly used terms in dataSource resolution in development environments
200
201fluid.defaults("kettle.dataSource.distributeDevTerms", {
202 gradeNames: ["fluid.component"],
203 distributeOptions: {
204 moduleTerms: {
205 record: "kettle.dataSource.file.moduleTerms",
206 target: "{that kettle.dataSource.file}.options.gradeNames"
207 }
208 }
209});