UNPKG

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