4 * An opinionated way to gather a source identifier to include in logged
5 * messages, using possibly too much information.
8 const path
= require('node:path');
9 const fs
= require('node:fs');
14 class FileScopeError
extends Error
{
15 constructor(...args
) {
17 Error
.captureStackTrace(this, this.constructor);
21 return this.constructor.name
;
27 * Read and parse package.json from a path.
28 * @param {String} packagePath
31 function readPackageJSON(packagePath
) {
33 const content
= fs
.readFileSync(path
.join(packagePath
, 'package.json')); // eslint-disable-line security/detect-non-literal-fs-filename
34 return JSON
.parse(content
);
45 * Returns whether path p exists.
49 function pathExists(p
) {
51 fs
.statSync(p
); // eslint-disable-line security/detect-non-literal-fs-filename
54 if (e
.code
!== 'ENOENT') {
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
68 function locatePackageBase(filename
) {
69 let currentPath
= filename
;
71 const d
= path
.dirname(currentPath
);
73 fs
.statSync(path
.join(d
, 'package.json')); // eslint-disable-line security/detect-non-literal-fs-filename
76 if (e
.code
!== 'ENOENT') {
81 } while (currentPath
!== '/');
82 throw new FileScopeError('unable to find package root');
87 * Get default options based on package directory structure.
88 * @param {String} packageBase
91 function defaultOptions(packageBase
) {
94 includePackage: false,
95 includeVersion: false,
97 _errorEncountered: false,
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;
113 options
._errorEncountered
= true;
114 options
.includePath
= false;
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}
136 function fileScope(filepath
, options
) {
137 let errorEncountered
= false;
138 let packageBase
= '';
140 packageBase
= locatePackageBase(filepath
);
142 errorEncountered
= true;
144 const defaults
= defaultOptions(packageBase
);
145 errorEncountered
|= defaults
._errorEncountered
;
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
;
166 const { base
, name
, ext
} = path
.parse(filepath
);
167 const isIndex
= (name
=== 'index') && ['.js', '.ts', '.mjs', '.cjs'].includes(ext
);
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
);
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
);
178 const components
= [errorEncountered
? errorPrefix : '', prefix
, packageIdentifier
, trimmedFilename
]
181 const scope
= components
.join(delimiter
);
183 return (name
) => `${scope}${delimiter}${name}`;