UNPKG

4.9 kBJavaScriptView Raw
1import { lstatSync, realpathSync } from "node:fs";
2import path from "node:path";
3import { fileURLToPath } from "node:url";
4import { setFlagsFromString } from "node:v8";
5import { runInNewContext } from "node:vm";
6import dotenv from "dotenv";
7import { environment, isProduction, refreshEnvironmentCache } from "./env.js";
8import { AppError } from "./error.js";
9import { isNil } from "./lodash.js";
10import {
11 loggerDetermineDefaultDestination,
12 loggerExtendGlobalContext,
13 newLogger,
14} from "./logger.js";
15import { _compasSentryExport } from "./sentry.js";
16
17/**
18 * Get the number of seconds since Unix epoch (1-1-1970).
19 *
20 * @since 0.1.0
21 *
22 * @returns {number}
23 */
24export function getSecondsSinceEpoch() {
25 return Math.floor(Date.now() / 1000);
26}
27
28/**
29 * A function that returns 'undefined'.
30 *
31 * @since 0.1.0
32 * @type {import("../types/advanced-types.d.ts").NoopFn}
33 */
34export function noop() {}
35
36/**
37 * Internal gc function reference.
38 * Note that this is undefined if the gc function is not called and Node is not running
39 * with --expose-gc on.
40 */
41let internalGc = global.gc;
42
43/**
44 * HACKY
45 *
46 * @since 0.1.0
47 *
48 * @returns {void}
49 */
50export function gc() {
51 if (isNil(internalGc)) {
52 setFlagsFromString("--expose_gc");
53 internalGc = runInNewContext("gc");
54 }
55
56 // @ts-ignore
57 internalGc();
58}
59
60/**
61 * Checks if the provided import.meta source is used as the project entrypoint.
62 * If so, reads the .env file, prepares the environmentCache, adds some handlers for
63 * uncaught exceptions, and calls the provided callback
64 *
65 * @since 0.1.0
66 * @summary Process entrypoint executor
67 *
68 * @param {ImportMeta} meta
69 * @param {(logger: import("./logger.js").Logger) => void|Promise<void>} cb
70 * @returns {void}
71 */
72export function mainFn(meta, cb) {
73 const { isMainFn, name } = isMainFnAndReturnName(meta);
74 if (!isMainFn) {
75 return;
76 }
77
78 // Load .env.local first, since existing values in `process.env` are not overwritten.
79 dotenv.config({ path: path.resolve(process.cwd(), ".env.local") });
80 dotenv.config();
81
82 refreshEnvironmentCache();
83
84 if (isProduction() && environment.APP_NAME) {
85 loggerExtendGlobalContext({
86 application: environment.APP_NAME,
87 });
88 }
89
90 loggerDetermineDefaultDestination();
91
92 const logger = newLogger({
93 ctx: { type: name },
94 });
95
96 const unhandled = (error) => {
97 logger.error(error);
98 process.exit(1);
99 };
100
101 process.on("unhandledRejection", (reason, promise) => {
102 if (_compasSentryExport) {
103 _compasSentryExport.captureException(reason);
104 }
105
106 unhandled({
107 type: "unhandledRejection",
108 reason: AppError.format(reason),
109 promise,
110 });
111 });
112
113 process.on("uncaughtException", (error, origin) => {
114 if (_compasSentryExport) {
115 _compasSentryExport.captureException(error);
116 }
117
118 unhandled({
119 type: "uncaughtException",
120 error: AppError.format(error),
121 origin,
122 });
123 });
124
125 process.on("warning", (warn) => {
126 logger.error({
127 type: "warning",
128 warning: AppError.format(warn),
129 });
130 });
131
132 Promise.resolve(cb(logger)).catch((e) => {
133 if (_compasSentryExport) {
134 _compasSentryExport.captureException(e);
135 }
136
137 unhandled({
138 type: "error",
139 message: "Error caught from callback passed in `mainFn`",
140 error: AppError.format(e),
141 });
142 });
143}
144
145/**
146 * ES module compatibility counterpart of the CommonJS __filename
147 *
148 * @since 0.1.0
149 *
150 * @param {ImportMeta} meta
151 * @returns {string}
152 */
153export function filenameForModule(meta) {
154 return fileURLToPath(meta.url);
155}
156
157/**
158 * ES module compatibility counterpart of the CommonJS __dirname
159 *
160 * @since 0.1.0
161 *
162 * @param {ImportMeta} meta
163 * @returns {string}
164 */
165export function dirnameForModule(meta) {
166 return path.dirname(filenameForModule(meta));
167}
168
169/**
170 * Checks if the provided meta.url is the process entrypoint and also returns the name of
171 * the entrypoint file
172 *
173 * @param {ImportMeta} meta
174 * @returns {{ isMainFn: boolean, name?: string}}
175 */
176export function isMainFnAndReturnName(meta) {
177 const modulePath = fileURLToPath(meta.url);
178
179 let scriptPath = process.argv[1];
180
181 // Support following symbolic links for node_modules/.bin items
182 const scriptStat = lstatSync(scriptPath);
183 if (scriptStat.isSymbolicLink()) {
184 scriptPath = realpathSync(scriptPath);
185 }
186 const scriptPathExt = path.extname(scriptPath);
187 if (scriptPathExt) {
188 return {
189 isMainFn: modulePath === scriptPath,
190 name: scriptPath
191 .substring(0, scriptPath.length - scriptPathExt.length)
192 .split(path.sep)
193 .pop(),
194 };
195 }
196
197 let modulePathWithoutExt = modulePath;
198 const modulePathExt = path.extname(modulePath);
199 if (modulePathExt) {
200 modulePathWithoutExt = modulePathWithoutExt.slice(0, -modulePathExt.length);
201 }
202
203 return {
204 isMainFn: modulePathWithoutExt === scriptPath,
205 name: modulePathWithoutExt.split(path.sep).pop(),
206 };
207}