@squeep/log-helper 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintrc.json ADDED
@@ -0,0 +1,89 @@
1
+ {
2
+ "env": {
3
+ "browser": false,
4
+ "es6": true,
5
+ "node": true
6
+ },
7
+ "extends": [
8
+ "eslint:recommended",
9
+ "plugin:node/recommended",
10
+ "plugin:security/recommended",
11
+ "plugin:sonarjs/recommended"
12
+ ],
13
+ "parserOptions": {
14
+ "ecmaVersion": "latest"
15
+ },
16
+ "plugins": [
17
+ "node",
18
+ "security",
19
+ "sonarjs"
20
+ ],
21
+ "rules": {
22
+ "array-element-newline": [
23
+ "error",
24
+ "consistent"
25
+ ],
26
+ "arrow-parens": [
27
+ "error",
28
+ "always"
29
+ ],
30
+ "arrow-spacing": [
31
+ "error",
32
+ {
33
+ "after": true,
34
+ "before": true
35
+ }
36
+ ],
37
+ "block-scoped-var": "error",
38
+ "block-spacing": "error",
39
+ "brace-style": "error",
40
+ "callback-return": "error",
41
+ "camelcase": "error",
42
+ "class-methods-use-this": "error",
43
+ "comma-dangle": [
44
+ "error",
45
+ "always-multiline"
46
+ ],
47
+ "comma-spacing": [
48
+ "error",
49
+ {
50
+ "after": true,
51
+ "before": false
52
+ }
53
+ ],
54
+ "comma-style": [
55
+ "error",
56
+ "last"
57
+ ],
58
+ "indent": [
59
+ "warn",
60
+ 2,
61
+ {
62
+ "SwitchCase": 1
63
+ }
64
+ ],
65
+ "sonarjs/cognitive-complexity": "warn",
66
+ "sonarjs/no-duplicate-string": "warn",
67
+ "keyword-spacing": "error",
68
+ "linebreak-style": [
69
+ "error",
70
+ "unix"
71
+ ],
72
+ "no-unused-vars": [
73
+ "error", {
74
+ "varsIgnorePattern": "^_"
75
+ }
76
+ ],
77
+ "object-curly-spacing": [
78
+ "error",
79
+ "always"
80
+ ],
81
+ "prefer-const": "error",
82
+ "quotes": [
83
+ "warn",
84
+ "single"
85
+ ],
86
+ "strict": "error",
87
+ "vars-on-top": "error"
88
+ }
89
+ }
package/.nycrc.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "reporter": [
3
+ "lcov",
4
+ "text"
5
+ ]
6
+ }
package/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # @squeep/log-helper
2
+
3
+ Utilities for standardized logging.
4
+
5
+ ## API
6
+
7
+ - `fileScope(filepath, options)`
8
+ Returns a function which will decorate function names with some combination of the source file path, package name, and package version. If the filepath is `index.js`, it will be replaced by its containing directory. Defaults for what is included are determined by Squeep opinions on package layouts.
9
+
10
+ Example:
11
+
12
+ ```javascript
13
+ // Assuming this file is located at 'project/lib/subdir/code.js'
14
+ // Assuming project/package.json defines 'name' as 'project'
15
+ const { fileScope } = require('@squeep/log-helper');
16
+ const _fileScope = fileScope(__filename, { includeVersion: false });
17
+
18
+ function example() {
19
+ const _scope = _fileScope('example');
20
+ console.log(_scope); // Will show 'project:subdir/code:example'
21
+ }
22
+ ```
23
+
24
+ Options:
25
+ - `includePackage` Package name from `package.json`.
26
+ - `includeVersion` Package version from `package.json`. If this is `true`, `includePackage` will also become `true`.
27
+ - `includePath` The path to the file, relative to the `package.json` file.
28
+ - `leftTrim` How much of the beginning of the path to elide. (e.g. 3 would render `foo/bar` as `/bar`)
29
+ - `delimiter` Placed between fields, defaults to `:`.
30
+ - `prefix` A field included before the package name.
31
+
32
+ Defaults, based on directory existing in project:
33
+ | | `src` | `lib` | other |
34
+ |----------------|-------|-------|-------|
35
+ | includePackage | false | true | false |
36
+ | includeVersion | false | true | false |
37
+ | includePath | true | true | false |
38
+ | leftTrim | 4 | 4 | 0 |
39
+
40
+ If any errors are encountered while trying to determine the path or package metadata, the scope will be prefixed with a '?'.
package/index.js ADDED
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ const { fileScope } = require('./lib/file-scope.js');
4
+
5
+ module.exports = {
6
+ fileScope,
7
+ };
@@ -0,0 +1,194 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * An opinionated way to gather a source identifier to include in logged
5
+ * messages, using possibly too much information.
6
+ */
7
+
8
+ const path = require('node:path');
9
+ const fs = require('node:fs');
10
+
11
+ /**
12
+ * Internal exception
13
+ */
14
+ class FileScopeError extends Error {
15
+ constructor(...args) {
16
+ super(...args);
17
+ Error.captureStackTrace(this, this.constructor);
18
+ }
19
+
20
+ get name() {
21
+ return this.constructor.name;
22
+ }
23
+ }
24
+
25
+
26
+ /**
27
+ * Read and parse package.json from a path.
28
+ * @param {String} packagePath
29
+ * @returns {Object}
30
+ */
31
+ function readPackageJSON(packagePath) {
32
+ try {
33
+ const content = fs.readFileSync(path.join(packagePath, 'package.json')); // eslint-disable-line security/detect-non-literal-fs-filename
34
+ return JSON.parse(content);
35
+ } catch (e) {
36
+ return {
37
+ name: '(unknown)',
38
+ version: '(unknown)',
39
+ };
40
+ }
41
+ }
42
+
43
+
44
+ /**
45
+ * Returns whether path p exists.
46
+ * @param {String} p
47
+ * @returns {Boolean}
48
+ */
49
+ function pathExists(p) {
50
+ try {
51
+ fs.statSync(p); // eslint-disable-line security/detect-non-literal-fs-filename
52
+ return true;
53
+ } catch (e) {
54
+ if (e.code !== 'ENOENT') {
55
+ throw e;
56
+ }
57
+ return false;
58
+ }
59
+ }
60
+
61
+
62
+ /**
63
+ * Walk up the path of provided filename until directory with
64
+ * package.json is located. We assume this is the package root.
65
+ * @param {String} filename
66
+ * @returns {String}
67
+ */
68
+ function locatePackageBase(filename) {
69
+ let currentPath = filename;
70
+ do {
71
+ const d = path.dirname(currentPath);
72
+ try {
73
+ fs.statSync(path.join(d, 'package.json')); // eslint-disable-line security/detect-non-literal-fs-filename
74
+ return d + '/';
75
+ } catch (e) {
76
+ if (e.code !== 'ENOENT') {
77
+ throw e;
78
+ }
79
+ }
80
+ currentPath = d;
81
+ } while (currentPath !== '/');
82
+ throw new FileScopeError('unable to find package root');
83
+ }
84
+
85
+
86
+ /**
87
+ * Get default options based on package directory structure.
88
+ * @param {String} packageBase
89
+ * @returns {Object}
90
+ */
91
+ function defaultOptions(packageBase) {
92
+ const options = {
93
+ includePath: false,
94
+ includePackage: false,
95
+ includeVersion: false,
96
+ leftTrim: 0,
97
+ _errorEncountered: false,
98
+ errorPrefix: '?',
99
+ delimiter: ':',
100
+ };
101
+
102
+ if (packageBase) {
103
+ try {
104
+ options.includePath = true;
105
+ if (pathExists(path.join(packageBase, 'lib'))) {
106
+ options.leftTrim = 4;
107
+ options.includePackage = true;
108
+ options.includeVersion = true;
109
+ } else if (pathExists(path.join(packageBase, 'src'))) {
110
+ options.leftTrim = 4;
111
+ }
112
+ } catch (e) {
113
+ options._errorEncountered = true;
114
+ options.includePath = false;
115
+ }
116
+ }
117
+
118
+ return options;
119
+ }
120
+
121
+
122
+ /**
123
+ * Returns a function suitable for decorating a function name with
124
+ * package information and details of the source file.
125
+ * @param {String} filepath full path from __filename
126
+ * @param {Object=} options
127
+ * @param {Boolean=} options.includePackage
128
+ * @param {Boolean=} options.includeVersion
129
+ * @param {Boolean=} options.includePath
130
+ * @param {String=} options.prefix
131
+ * @param {Number=} options.leftTrim
132
+ * @param {String=} options.errorPrefix
133
+ * @param {String=} options.delimiter
134
+ * @returns {Function}
135
+ */
136
+ function fileScope(filepath, options) {
137
+ let errorEncountered = false;
138
+ let packageBase = '';
139
+ try {
140
+ packageBase = locatePackageBase(filepath);
141
+ } catch (e) {
142
+ errorEncountered = true;
143
+ }
144
+ const defaults = defaultOptions(packageBase);
145
+ errorEncountered |= defaults._errorEncountered;
146
+ const {
147
+ includePackage,
148
+ includeVersion,
149
+ includePath,
150
+ prefix,
151
+ leftTrim,
152
+ errorPrefix,
153
+ delimiter,
154
+ } = {
155
+ ...defaults,
156
+ ...options,
157
+ };
158
+
159
+ let packageIdentifier;
160
+ if (includePackage || includeVersion) {
161
+ const { name: packageName, version: packageVersion } = readPackageJSON(packageBase);
162
+ // including version implies including package
163
+ packageIdentifier = includeVersion ? `${packageName}@${packageVersion}` : packageName;
164
+ }
165
+
166
+ const { base, name, ext } = path.parse(filepath);
167
+ const isIndex = (name === 'index') && ['.js', '.ts', '.mjs', '.cjs'].includes(ext);
168
+
169
+ // If filepath is an index, trim off filename and last slash, otherwise just trim off extension
170
+ const rightTrim = 0 - (isIndex ? (base.length + 1) : ext.length);
171
+ // We can't trim both ends at once as dirname won't work when isIndex is true
172
+ const rightTrimmed = filepath.slice(0, rightTrim);
173
+
174
+ // Trim leading part of path
175
+ const trim = (includePath && packageBase) ? (leftTrim + packageBase.length) : (path.dirname(rightTrimmed).length + 1);
176
+ const trimmedFilename = rightTrimmed.slice(trim);
177
+
178
+ const components = [errorEncountered ? errorPrefix : '', prefix, packageIdentifier, trimmedFilename]
179
+ .filter((x) => x);
180
+
181
+ const scope = components.join(delimiter);
182
+
183
+ return (name) => `${scope}${delimiter}${name}`;
184
+ }
185
+
186
+
187
+ module.exports = {
188
+ FileScopeError,
189
+ readPackageJSON,
190
+ pathExists,
191
+ locatePackageBase,
192
+ defaultOptions,
193
+ fileScope,
194
+ };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@squeep/log-helper",
3
+ "version": "1.0.0",
4
+ "description": "Simple helpers for standardized logging",
5
+ "main": "index.js",
6
+ "engines": {
7
+ "node": ">=14"
8
+ },
9
+ "directories": {
10
+ "lib": "lib",
11
+ "test": "test"
12
+ },
13
+ "scripts": {
14
+ "coverage": "nyc npm test",
15
+ "coverage-check": "nyc check-coverage",
16
+ "eslint": "eslint *.js lib test",
17
+ "test": "mocha --recursive"
18
+ },
19
+ "keywords": [],
20
+ "author": "Justin Wind <jwind-npm@squeep.com>",
21
+ "license": "ISC",
22
+ "pre-commit": [
23
+ "eslint",
24
+ "coverage",
25
+ "coverage-check"
26
+ ],
27
+ "devDependencies": {
28
+ "eslint": "^8.53.0",
29
+ "eslint-plugin-node": "^11.1.0",
30
+ "eslint-plugin-security": "^1.7.1",
31
+ "eslint-plugin-sonarjs": "^0.23.0",
32
+ "mocha": "^10.2.0",
33
+ "nyc": "^15.1.0",
34
+ "pre-commit": "^1.2.2",
35
+ "sinon": "^17.0.1"
36
+ }
37
+ }
@@ -0,0 +1,282 @@
1
+ /* eslint-env mocha */
2
+ 'use strict';
3
+
4
+ const assert = require('assert');
5
+ const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
6
+ const fs = require('fs');
7
+ const {
8
+ readPackageJSON,
9
+ fileScope,
10
+ locatePackageBase,
11
+ defaultOptions,
12
+ pathExists,
13
+ FileScopeError,
14
+ } = require('../../lib/file-scope');
15
+
16
+ describe('File Scope', function () {
17
+ let noentError;
18
+
19
+ before(function () {
20
+ noentError = new Error('ENOENT: no such file or directory');
21
+ Object.assign(noentError, {
22
+ errno: -2,
23
+ syscall: 'stat',
24
+ code: 'ENOENT',
25
+ });
26
+ });
27
+
28
+ afterEach(function () {
29
+ sinon.restore();
30
+ });
31
+
32
+ describe('FileScopeError', function () {
33
+ it('covers', function () {
34
+ const e = new FileScopeError('beep');
35
+ assert.strictEqual(e.name, 'FileScopeError');
36
+ });
37
+ }); // FileScopeError
38
+
39
+ describe('readPackageJSON', function () {
40
+ let filepath;
41
+ beforeEach(function () {
42
+ filepath = '/path/to/package.json';
43
+ sinon.stub(fs, 'readFileSync').returns(`{
44
+ "name": "@example/package",
45
+ "version": "3.1.4"
46
+ }`);
47
+ });
48
+ it('covers success', function () {
49
+ const result = readPackageJSON(filepath);
50
+ assert.strictEqual(result.name, '@example/package');
51
+ assert.strictEqual(result.version, '3.1.4');
52
+ });
53
+ it('covers failure', function () {
54
+ fs.readFileSync.throws();
55
+ const result = readPackageJSON(filepath);
56
+ assert.strictEqual(result.name, '(unknown)');
57
+ assert.strictEqual(result.version, '(unknown)');
58
+ });
59
+ }); // readJSONFile
60
+
61
+ describe('pathExists', function () {
62
+ let p;
63
+ beforeEach(function () {
64
+ p = '/path/to/package';
65
+ sinon.stub(fs, 'statSync').returns();
66
+ });
67
+ it('returns true when path exists', function () {
68
+ const result = pathExists(p);
69
+ assert.strictEqual(result, true);
70
+ });
71
+ it('returns false when path does not exist', function () {
72
+ fs.statSync.throws(noentError);
73
+ const result = pathExists(p);
74
+ assert.strictEqual(result, false);
75
+ });
76
+ it('raises other error', function () {
77
+ const expectedError = new Error('oh no');
78
+ fs.statSync.throws(expectedError);
79
+ assert.throws(() => pathExists(p), expectedError);
80
+ });
81
+ }); // pathExists
82
+
83
+ describe('locatePackageBase', function () {
84
+ let filepath;
85
+
86
+ beforeEach(function () {
87
+ filepath = '/path/to/package/lib/file.js';
88
+ sinon.stub(fs, 'statSync');
89
+ });
90
+
91
+ it('covers unstubbed result', function () {
92
+ sinon.restore();
93
+ locatePackageBase(__filename);
94
+ });
95
+
96
+ it('locates the package base', function () {
97
+ fs.statSync
98
+ .onCall(0).throws(noentError)
99
+ .onCall(1).returns()
100
+ ;
101
+ const result = locatePackageBase(filepath);
102
+ assert.strictEqual(result, '/path/to/package/');
103
+ });
104
+
105
+ it('cannot locate the package base', function () {
106
+ fs.statSync.throws(noentError);
107
+ assert.throws(() => locatePackageBase(filepath), FileScopeError);
108
+ });
109
+
110
+ it('propagates unknown error', function () {
111
+ const expectedException = new Error('oh no');
112
+ fs.statSync
113
+ .onCall(0).throws(noentError)
114
+ .onCall(1).throws(expectedException)
115
+ ;
116
+ assert.throws(() => locatePackageBase(filepath), expectedException);
117
+ });
118
+ }); // locatePackageBase
119
+
120
+ describe('defaultOptions', function () {
121
+ let packageBase, expected;
122
+ beforeEach(function () {
123
+ packageBase = '/path/to/package';
124
+ sinon.stub(fs, 'statSync').returns();
125
+ expected = {
126
+ includePath: false,
127
+ includePackage: false,
128
+ includeVersion: false,
129
+ leftTrim: 0,
130
+ _errorEncountered: false,
131
+ errorPrefix: '?',
132
+ delimiter: ':',
133
+ };
134
+ });
135
+ it('covers no path', function () {
136
+ const options = defaultOptions();
137
+ assert.deepStrictEqual(options, expected);
138
+ });
139
+ it('covers default', function () {
140
+ expected.includePath = true;
141
+ fs.statSync.throws(noentError);
142
+ const options = defaultOptions(packageBase);
143
+ assert.deepStrictEqual(options, expected);
144
+ });
145
+ it('covers lib package', function () {
146
+ expected.includePath = true;
147
+ expected.includePackage = true;
148
+ expected.includeVersion = true;
149
+ expected.leftTrim = 4;
150
+ const options = defaultOptions(packageBase);
151
+ assert.deepStrictEqual(options, expected);
152
+ });
153
+ it('covers src package', function () {
154
+ expected.includePath = true;
155
+ expected.leftTrim = 4;
156
+ fs.statSync.onCall(0).throws(noentError);
157
+ const options = defaultOptions(packageBase);
158
+ assert.deepStrictEqual(options, expected);
159
+ });
160
+ it('covers error', function () {
161
+ const expectedError = new Error('oh no');
162
+ fs.statSync.throws(expectedError);
163
+ expected._errorEncountered = true;
164
+ const options = defaultOptions(packageBase);
165
+ assert.deepStrictEqual(options, expected);
166
+ });
167
+ }); // defaultOptions
168
+
169
+ describe('fileScope', function () {
170
+ let filepath, options, method;
171
+ beforeEach(function () {
172
+ filepath = '/path/to/package/lib/deep/file.js';
173
+ sinon.stub(fs, 'statSync')
174
+ .onCall(0).throws(noentError) // deep
175
+ .onCall(1).throws(noentError) // lib
176
+ .onCall(2).returns() // packageBase
177
+ ;
178
+ sinon.stub(fs, 'readFileSync').returns(`{
179
+ "name": "@example/package",
180
+ "version": "3.1.4"
181
+ }`);
182
+ options = {
183
+ includePath: true,
184
+ includePackage: false,
185
+ includeVersion: false,
186
+ };
187
+ method = 'method';
188
+ });
189
+
190
+ it('defaults', function () {
191
+ filepath = '/path/to/package/code/module/file.js';
192
+ fs.statSync
193
+ .onCall(3).throws(noentError) // no lib
194
+ .onCall(4).throws(noentError) // no src
195
+ ;
196
+ const result = fileScope(filepath)(method);
197
+ assert.strictEqual(result, 'code/module/file:method');
198
+ });
199
+
200
+ it('everything', function () {
201
+ options.includePath = true;
202
+ options.includePackage = true;
203
+ options.includeVersion = true;
204
+ const result = fileScope(filepath, options)(method);
205
+ assert.strictEqual(result, '@example/package@3.1.4:deep/file:method');
206
+ });
207
+
208
+ it('minimal', function () {
209
+ options.includePath = false;
210
+ options.includePackage = false;
211
+ options.includeVersion = false;
212
+ const result = fileScope(filepath, options)(method);
213
+ assert.strictEqual(result, 'file:method');
214
+ });
215
+
216
+ it('package only', function () {
217
+ options.includePath = false;
218
+ options.includePackage = true;
219
+ options.includeVersion = false;
220
+ const result = fileScope(filepath, options)(method);
221
+ assert.strictEqual(result, '@example/package:file:method');
222
+ });
223
+
224
+ it('covers no package root', function () {
225
+ options.includePackage = true;
226
+ fs.statSync.restore()
227
+ sinon.stub(fs, 'statSync').throws(noentError);
228
+ fs.readFileSync.throws(noentError);
229
+ const result = fileScope(filepath, options)(method);
230
+ assert.strictEqual(result, '?:(unknown):file:method');
231
+ });
232
+
233
+ it('covers exception while finding package root', function () {
234
+ const expectedException = new Error('oh no');
235
+ options.includePackage = true;
236
+ fs.statSync.restore()
237
+ sinon.stub(fs, 'statSync').throws(expectedException);
238
+ fs.readFileSync.throws(noentError);
239
+ const result = fileScope(filepath, options)(method);
240
+ assert.strictEqual(result, '?:(unknown):file:method');
241
+ });
242
+
243
+ it('handles index.js', function () {
244
+ filepath = '/path/to/package/src/deep/index.js';
245
+ fs.statSync
246
+ .onCall(3).throws(noentError) // no lib
247
+ .onCall(4).returns() // src
248
+ ;
249
+ const result = fileScope(filepath)(method);
250
+ assert.strictEqual(result, 'deep:method');
251
+ });
252
+
253
+ it('handles index.js when including path', function () {
254
+ filepath = '/path/to/package/code/folder/deep/index.js';
255
+ fs.statSync.restore();
256
+ sinon.stub(fs, 'statSync')
257
+ .throws(noentError)
258
+ .onCall(3).returns() // packageBase found
259
+ // no lib
260
+ // no src
261
+ ;
262
+ const result = fileScope(filepath)(method);
263
+ assert.strictEqual(result, 'code/folder/deep:method');
264
+ });
265
+
266
+ it('handles index.js when not including path', function () {
267
+ filepath = '/path/to/package/code/folder/deep/index.ts';
268
+ fs.statSync.restore();
269
+ sinon.stub(fs, 'statSync')
270
+ .throws(noentError)
271
+ .onCall(3).returns() // packageBase found
272
+ // no lib
273
+ // no src
274
+ ;
275
+ const result = fileScope(filepath, { includePath: false })(method);
276
+ assert.strictEqual(result, 'deep:method');
277
+ });
278
+
279
+ }); // fileScope
280
+
281
+
282
+ }); // File Scope