Skip to content

Instantly share code, notes, and snippets.

@aarhusgregersen
Last active June 16, 2024 10:00
Show Gist options
  • Save aarhusgregersen/54fdf463b7e000e64365cc4bd096f67d to your computer and use it in GitHub Desktop.
Save aarhusgregersen/54fdf463b7e000e64365cc4bd096f67d to your computer and use it in GitHub Desktop.
CJS to ESM Conversion
import RedisStore from "connect-redis";
import cors from "cors";
import "dotenv/config.js";
import express from "express";
import expressSession from "express-session";
import useragent from "express-useragent";
import helmet from "helmet";
import morgan from "morgan";
import passport from "passport";
import path from "path";
import { authRoutes } from "#src/common/auth/auth.js";
import { checkToken } from "#src/common/auth/helpers/authHelper.js";
import { corsSettings } from "#src/common/auth/helpers/corsSettings.js";
import whilelistDomains from "#src/common/auth/helpers/whitelist.json" assert {
type: "json",
};
import { redisClient } from "#src/common/helpers/db.js";
import { logResBody, notifyError } from "#src/common/helpers/utils.js";
import { jobRunner } from "#src/common/jobs/jobRunner.js";
import { getLoggerFromFilename } from "#src/common/logging/index.js";
import makeRequestLoggerMiddleware from "#src/common/logging/requestLoggerMiddleware.js";
import { trimmer } from "#src/common/utilities/trimmer.js";
import { ChatController } from "#src/network/controller/chatController.js";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const filepath = fileURLToPath(new URL("app.js", import.meta.url));
const app = express();
app.use("/__checkstatus__", (_req: express.Request, res: express.Response) => {
res.status(200).send("");
});
app.set("trust proxy", true);
app.set("views", path.join(__dirname, "views"));
// Improve security
app.use(helmet());
// Prep useragent
app.use(useragent.express());
/* Cookie Parser - try with both on/off setting */
// app.use(cookieParser());
// Support application/scim+json
app.use(
express.json({
type: "application/*+json",
}),
);
// Support application/json
app.use(express.json());
app.use(
express.urlencoded({
extended: true,
}),
);
const docsHelmetSecurityConfig = helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-eval'"], // Add 'unsafe-eval' here
// Add other directives as needed
},
},
});
// Serve static files from 'public/docs'. This includes CSS, JS, images, etc.
app.use(
"/docs",
docsHelmetSecurityConfig,
express.static(path.join(__dirname, "/public/docs")),
);
// Serve the 'index.html' directly for the '/docs' route.
app.get("/docs", (req: express.Request, res: express.Response) => {
res.sendFile("index.html", { root: path.join(__dirname, "/public/docs") });
});
// Splitting express session definition to allow unsecure cookies in development
const sess: expressSession.SessionOptions = {
store: new RedisStore({
client: redisClient,
prefix: "myapp:",
}),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {},
};
// Testing if we can live without this recommended option, for a while. Disabling it makes SSO work!
// See more: https://www.npmjs.com/package/express-session#cookiesecure
// if (app.get('env') === 'production' ) {
// app.set('trust proxy', 1) // trust first proxy
// sess.cookie.secure = true // serve secure cookies
// }
app.use(express.static(path.join(__dirname, "public")));
app.use(expressSession(sess));
app.use(passport.initialize());
app.use(passport.session());
const corsOptions: cors.CorsOptions = {
allowedHeaders: [
"Origin",
"X-Requested-With",
"Content-Type",
"Accept",
"X-Access-Token",
"x-user_id",
],
credentials: true,
methods: "GET,HEAD,OPTIONS,PUT,PATCH,POST,DELETE",
preflightContinue: false,
};
// Setup CORS
app.use(cors(corsOptions));
// Manual cors configuration for subdomains etc.
app.use(corsSettings);
jobRunner();
const log = getLoggerFromFilename(filepath);
app.use(makeRequestLoggerMiddleware(log));
app.use(logResBody);
// Morgan logger
app.use(morgan("dev", { immediate: true }));
// Open api's
import { authenticateRoutes } from "#src/common/auth/security/authenticate.js";
import { openAPIRoutes } from "./src/common/open-api.js";
import { fastTrackAssignmentRouter } from "./src/fastTrack/fastTrackAssignment/fastTrackAssignment.routes.js";
import { webhookEmply } from "./src/webhooks/webhookEmply.js";
app.use("/auth", authRoutes); // No token or permission needed
app.use("/open-api", openAPIRoutes); // No token or permission needed
app.use("/security", authenticateRoutes); // No token or permission needed
app.use("/api/fast-track/assignment", [fastTrackAssignmentRouter]);
// Webhooks (Insecure / Unrestricted)
app.post("/webhooks/:org_id/emply", [webhookEmply]);
// Get Locale routes
import { getLocales } from "./src/common/locale/getLocales.js";
import { locale } from "./src/common/locale/locale.js";
import { scimContentType } from "./src/scim/helpers/scimContentType.js";
import scim from "./src/scim/scim.routes.js";
/* Get language json */
app.get("/api/locale/:lang", [locale]);
app.get("/api/dictionary/languages", [getLocales]);
import { route as router } from "./src/common/router.js";
import { route as mediaRoutes } from "./src/media/media.js";
import { moduleRoutes } from "./src/modules/modules.routes.js";
// Secured api's
app.use("/api/", [checkToken], trimmer); // Security middleware, Remove trailing spaces
app.use("/api/scim/v2", [scimContentType, scim]);
app.use("/api", router);
app.use("/api", mediaRoutes);
app.use("/api", moduleRoutes);
// TODO: Change this to properly respond if user reaches and address that isn't part of the API. However, it should still respond in the positive to uptime checks (maybe a separate address for that)
app.use("/", (_req: express.Request, res: express.Response) => {
res.status(200).json({ message: "introDus API" });
});
app.use((req: express.Request, res: any, next: express.NextFunction) => {
const err = new Error("Not Found");
const fullUrl = `${req.protocol}://${req.get("host")}${req.originalUrl}`;
const method = req.method;
res.statusCode = 404;
res.status = 404;
res.method = method;
res.fullUrl = fullUrl;
next(err);
});
app.use(
(
err: any,
req: express.Request,
res: express.Response,
_next: express.NextFunction,
) => {
const error: string = typeof err === "string" ? err : err.err;
const message: string = typeof err === "string" ? err : err.message;
const status_code: number = err.status || 400;
// If development, log the error otherwise only log if its not a 404
if (app.get("env") === "development") {
console.warn("message: ", message);
console.warn("status_code: ", status_code);
console.warn("error: ", error);
} else if (status_code !== 404) {
notifyError(req, err);
}
res.statusCode = status_code;
res.json({
success: false,
message: typeof err === "string" ? err : message,
error: app.get("env") === "development" ? error : null,
});
},
);
// Get port from environment and store in Express.
const port = normalizePort(process.env.PORT);
app.set("port", port);
// Listen on provided port, on all network interfaces.
const server = app.listen(port, function () {
log.info(
`Listening on ${process.env.ENV_URL_API} on ${process.env.NODE_ENV}`,
);
});
// Normalize a port into a number, string, or false.
function normalizePort(val: any): number | string | false {
const port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
// web socket cconnection
import { Server } from "socket.io";
const io = new Server(server, {
cors: {
origin: whilelistDomains.whitelist_url,
methods: ["GET", "POST"],
},
});
io.on("connection", (socket: any) => {
// NOTE :: USE socket.rooms for network groups
socket.join("chat room");
socket.broadcast.emit(
"SERVER_BRODCAST_MESSAGE",
"One member joined the chat.",
);
console.log("a user connected!");
socket.on("CLIENT_MESSAGE", (message_param: any) => {
//async save chat message
ChatController.saveChatMessage(message_param);
socket.broadcast.emit("SERVER_BRODCAST_MESSAGE", message_param);
});
socket.on("disconnect", () => {
socket.broadcast.emit(
"SERVER_BRODCAST_MESSAGE",
"One member Left the chat. and is offline",
);
console.log("a user disconnected!");
});
});
export default app;
import AWS from "aws-sdk";
// rome-ignore lint/suspicious/noShadowRestrictedNames: <explanation>
import Promise from "bluebird";
import { Redis } from "ioredis";
import mongoose from "mongoose";
// Promisify mongoose
mongoose.Promise = Promise;
// Prep s3
const s3Config: AWS.S3.ClientConfiguration = {
signatureVersion: process.env.S3_SIGNATURE_VERSION,
region: process.env.S3_REGION,
endpoint: process.env.S3_ENDPOINT,
};
let redisClient: Redis;
let mongoURI;
if (process.env.NODE_ENV === "production") {
redisClient = new Redis(process.env.REDIS_URL);
mongoURI = process.env.MONGODB_ADDON_URI;
} else if (process.env.NODE_ENV === "staging") {
redisClient = new Redis(process.env.REDIS_URL);
mongoURI = `mongodb+srv://${process.env.MONGODB_USERNAME}:${process.env.MONGODB_PASSWORD}@${process.env.MONGODB_SERVER}/${process.env.MONGODB_DATABASE}${process.env.MONGODB_SETTINGS}`;
} else {
redisClient = new Redis();
mongoURI = `mongodb+srv://${process.env.MONGODB_USERNAME}:${process.env.MONGODB_PASSWORD}@${process.env.MONGODB_SERVER}/${process.env.MONGODB_DATABASE}${process.env.MONGODB_SETTINGS}`;
}
const s3: AWS.S3 = new AWS.S3(s3Config);
const mongoOpt: mongoose.ConnectOptions = {};
if (process.env.MONGODB_AUTHSOURCE) {
mongoOpt.authSource = process.env.MONGODB_AUTHSOURCE;
}
// mongoose.set('debug', true);
mongoose.connect(mongoURI, mongoOpt);
// Connect to redis
redisClient
.connect()
.then(() => console.log("Connected to Redis"))
.catch(console.error);
export { redisClient, mongoose, s3 };
import schedule from "node-schedule";
import { getFastTracks } from "./processes/fastTrackScheduler.process.js";
import { getNetworkUnreads } from "./processes/networkUnreads.process.js";
import { getNewTimelineSections } from "./processes/newTimelineSections.process.js";
import { getCompletedTimelinesWithScheduledAssignment } from "./processes/scheduledAssignments.process.js";
import { findAndSendMessages } from "./processes/sendMessage.process.js";
import { getTaskAssignments } from "./processes/taskReminders.process.js";
import { getTimelineReminders } from "./processes/timelineReminders.process.js";
import { getActivationReminders } from "./processes/userActivationReminder.process.js";
import { getUsersToDelete } from "./processes/userDelete.process.js";
const env = process.env.NODE_ENV || "development";
export const jobRunner = async () => {
try {
if (env === "production") {
console.log(`[JOBS] Jobs being scheduled in ${env}...`);
schedule.scheduleJob("30 1 * * 1-7", (firedate) => {
try {
getUsersToDelete();
} catch (err) {
console.log("[JOB] User deletion job failed with error: ", err);
}
});
schedule.scheduleJob("0 2 * * 1-7", (firedate) => {
try {
getCompletedTimelinesWithScheduledAssignment();
} catch (err) {
console.log("[JOB] Scheduled Assignment failed with error: ", err);
}
});
schedule.scheduleJob("30 2 * * 1-7", (firedate) => {
try {
getActivationReminders();
} catch (err) {
console.log(
"[JOB] User Activation Reminders failed with error: ",
err,
);
}
});
schedule.scheduleJob("0 3 * * 1-7", (firedate) => {
try {
getTimelineReminders();
} catch (err) {
console.log("[JOB] Timeline Reminders failed with error: ", err);
}
});
schedule.scheduleJob("30 3 * * 1-7", (firedate) => {
try {
getNewTimelineSections();
} catch (err) {
console.log(
"[JOB] Timeline Section Notification failed with error: ",
err,
);
}
});
schedule.scheduleJob("0 4 * * 1-7", (firedate) => {
try {
getNetworkUnreads();
} catch (err) {
console.log("[JOB] Network Unreads failed with error: ", err);
}
});
schedule.scheduleJob("30 4 * * 1-7", (firedate) => {
try {
getTaskAssignments();
} catch (err) {
console.log("[JOB] Task Reminders failed with error: ", err);
}
});
schedule.scheduleJob("0 5 * * 1-7", (firedate) => {
try {
getFastTracks();
} catch (err) {
console.log("[JOB] Get Fast Tracks failed with error: ", err);
}
});
// TODO: Fix task digests process before implementing
// schedule.scheduleJob("30 5 * * 1-7", (firedate) => {
// try {
// getTaskDigests()
// } catch (err) {
// console.log("[JOB] Task Reminders failed with error: ", err);
// }
// });
// Only runs 1-5 (mon-fri) so we don't send messages in weekends
// Runs between 7-15 UTC
schedule.scheduleJob("*/30 7-15 * * 1-5", (firedate) => {
try {
findAndSendMessages();
} catch (err) {
console.log("[JOB] Sending Messages job failed with error: ", err);
}
});
} else {
// console.log(`[JOBS] Not running in ${env}`);
// getUsersToDelete();
// getNetworkUnreads();
// getTaskAssignments();
// getTimelineReminders();
// getNewTimelineSections();
// findAndSendMessages();
// Be aware that e.g. emails will be "sent" through ethereal.email (so won't be received by the email)
// but db changes will function as normal.
// Uncomment the lines below to test jobs in dev.
// NB: This will mean they run right away, instead of on cron
// runScheduledAssignmentsJob();
// runActivationRemindersJob();
// runRemindersJob();
// runNewTimelineSectionsJob();
// runTaskRemindersJob();
// runTaskDigestJob();
// runNetworkUnreadsJob();
// runRemindersJob();
// getFastTracks();
// runSendMessageJob();
}
} catch (err) {
console.log(`${env.toUpperCase}: Could not start job runner.`);
console.log(err);
}
};
{
"name": "backend",
"version": "1.0.0",
"engines": {
"node": ">=20.10.0"
},
"private": true,
"release": {
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/git"
],
"branches": [
"master"
]
},
"type": "module",
"exports": "./dist/app.js",
"imports": {
"#src/*": "./dist/src/*",
"#reports/*": "./dist/reports/*"
},
"scripts": {
"start": "node --import=./tsnode.esm.js --enable-source-maps app.ts",
"start:old": "node --import=ts-node/esm --watch ./app.ts",
"typecheck": "tsc --noEmit",
"build": "tsc",
"lint": "npx rome format ./src --write",
"test": "jest",
"docs": "node ./node_modules/gulp/bin/gulp apidoc",
"staging": "NODE_ENV=staging node dist/app.js",
"production": "NODE_ENV=production node dist/app.js"
},
"devDependencies": {
"@semantic-release/changelog": "xx.xx",
"@semantic-release/commit-analyzer": "xx.xx",
"@semantic-release/git": "xx.xx",
"@semantic-release/release-notes-generator": "xx.xx",
"@types/connect-redis": "xx.xx",
"@types/html-pdf-node": "xx.xx",
"@types/jest": "xx.xx",
"@types/passport-azure-ad": "xx.xx",
"@types/passport-local": "xx.xx",
"@types/supertest": "xx.xx",
"rome": "xx.xx",
"semantic-release": "xx.xx",
"supertest": "xx.xx",
"ts-jest": "xx.xx",
"ts-node": "xx.xx"
},
"dependencies": {
"@aws-sdk/client-s3": "xx.xx",
"@sendinblue/client": "xx.xx",
"@typegoose/typegoose": "xx.xx",
"@types/bcrypt-nodejs": "xx.xx",
"@types/bluebird": "xx.xx",
"@types/cors": "xx.xx",
"@types/date-and-time": "xx.xx",
"@types/email-templates": "xx.xx",
"@types/express": "xx.xx",
"@types/express-session": "xx.xx",
"@types/express-useragent": "xx.xx",
"@types/geoip-lite": "xx.xx",
"@types/jsonwebtoken": "xx.xx",
"@types/lodash": "xx.xx",
"@types/mime": "xx.xx",
"@types/morgan": "xx.xx",
"@types/multer": "xx.xx",
"@types/node": "xx.xx",
"@types/node-schedule": "xx.xx",
"@types/on-finished": "xx.xx",
"@types/passport": "xx.xx",
"@types/request": "xx.xx",
"@types/request-promise": "xx.xx",
"@types/sharp": "xx.xx",
"@types/uuid": "xx.xx",
"aws-sdk": "xx.xx",
"axios": "xx.xx",
"bcrypt-nodejs": "xx.xx",
"bluebird": "xx.xx",
"chart.js": "xx.xx",
"cjstoesm": "xx.xx",
"class-transformer": "xx.xx",
"class-validator": "xx.xx",
"colors": "xx.xx",
"connect-redis": "xx.xx",
"cors": "xx.xx",
"date-and-time": "xx.xx",
"dayjs": "xx.xx",
"dotenv": "xx.xx",
"email-templates": "xx.xx",
"excel4node": "xx.xx",
"express": "xx.xx",
"express-session": "xx.xx",
"express-useragent": "xx.xx",
"geoip-lite": "xx.xx",
"gulp": "xx.xx",
"gulp-apidoc": "xx.xx",
"handlebars": "xx.xx",
"helmet": "xx.xx",
"html-pdf-node": "xx.xx",
"html-to-text": "xx.xx",
"ioredis": "xx.xx",
"jest": "xx.xx",
"jsonwebtoken": "xx.xx",
"lodash": "xx.xx",
"lorem-ipsum": "xx.xx",
"mime": "xx.xx",
"mime-types": "xx.xx",
"moment": "xx.xx",
"moment-random": "xx.xx",
"moment-range": "xx.xx",
"moment-timezone": "xx.xx",
"mongoose": "xx.xx",
"morgan": "xx.xx",
"multer": "xx.xx",
"multer-s3-transform": "xx.xx",
"node-schedule": "xx.xx",
"nodemailer": "xx.xx",
"on-finished": "xx.xx",
"passport": "xx.xx",
"passport-azure-ad": "xx.xx",
"passport-local": "xx.xx",
"passport-magic-login": "xx.xx",
"request-promise": "xx.xx",
"sharp": "xx.xx",
"socket.io": "xx.xx",
"tlds": "xx.xx",
"typescript": "xx.xx",
"uuid": "xx.xx",
"winston": "xx.xx",
"winston-elasticsearch": "xx.xx",
"winston-format-debug": "xx.xx"
}
}
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Node 20",
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
"incremental": true /* Enable incremental compilation */,
"target": "ESNext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
"module": "ESNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
"lib": [
"ESNext"
] /* Specify library files to be included in the compilation. */,
"allowJs": true /* Allow javascript files to be compiled. */,
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"declaration": true /* Generates corresponding '.d.ts' file. */,
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true /* Generates corresponding '.map' file. */,
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "dist" /* Redirect output structure to the directory. */,
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
"removeComments": true /* Do not emit comments to output. */,
// "noEmit": true, /* Do not emit outputs. */
"importHelpers": true /* Import emit helpers from 'tslib'. */,
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ Defaults to true with 'strict' on
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
"noUnusedLocals": true /* Report errors on unused locals. */,
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
/* Module Resolution Options */
"moduleResolution": "nodenext" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
"baseUrl": "./" /* Base directory to resolve non-absolute module names. */,
"paths": {
"#src/*": ["src/*"],
"#reports/*": ["reports/*"]
},
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
"typeRoots": [
"./types"
] /* List of folders to include type definitions from. */,
// "types": [], /* Type declaration files to be included in compilation. */
"allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
"resolveJsonModule": true /* Include .json files in compilation. Used with include: []. */,
/* Source Map Options */
"sourceRoot": "./" /* Specify the location where debugger should locate TypeScript files instead of source locations. */,
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
"inlineSources": true /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */,
/* Experimental Options */
"experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
"emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */,
// EmitDecorator helps typegoose: https://typegoose.github.io/typegoose/docs/guides/use-without-emitDecoratorMetadata/#advantages-to-emitdecoratormetadata
/* Advanced Options */
"skipLibCheck": true /* Skip type checking of declaration files. */,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
},
"ts-node": {
"esm": true
},
"exclude": [
"node_modules",
"**/*.spec.ts",
"dist",
"logs",
"app.js/",
"gulpfile.js",
"test"
]
}

Running npm start without first compiling with npm run build

node --import=./tsnode.esm.js --enable-source-maps app.ts

Gives:

node:internal/process/esm_loader:40
      internalBinding('errors').triggerUncaughtException(
                                ^
Error: Cannot find module '/Users/aarhus/code/introdus/backend/dist/src/common/auth/auth.js' imported from /Users/aarhus/code/introdus/backend/app.ts
    at finalizeResolution (/Users/aarhus/code/introdus/backend/node_modules/ts-node/dist-raw/node-internal-modules-esm-resolve.js:366:11)
    at moduleResolve (/Users/aarhus/code/introdus/backend/node_modules/ts-node/dist-raw/node-internal-modules-esm-resolve.js:801:10)
    at Object.defaultResolve (/Users/aarhus/code/introdus/backend/node_modules/ts-node/dist-raw/node-internal-modules-esm-resolve.js:912:11)
    at /Users/aarhus/code/introdus/backend/node_modules/ts-node/src/esm.ts:218:35
    at entrypointFallback (/Users/aarhus/code/introdus/backend/node_modules/ts-node/src/esm.ts:168:34)
    at /Users/aarhus/code/introdus/backend/node_modules/ts-node/src/esm.ts:217:14
    at addShortCircuitFlag (/Users/aarhus/code/introdus/backend/node_modules/ts-node/src/esm.ts:409:21)
    at resolve (/Users/aarhus/code/introdus/backend/node_modules/ts-node/src/esm.ts:197:12)
    at nextResolve (node:internal/modules/esm/hooks:865:28)
    at Hooks.resolve (node:internal/modules/esm/hooks:303:30)
    at /Users/aarhus/code/introdus/backend/node_modules/ts-node/src/esm.ts:218:35

This is a little concerning as it shouldn't be pointing to the dist directory, if I'm trying to run using ts-node, should it? Worst case, it should be at least compiling the code before attempting to run it. This happens regardless if I have transpileOnly to true or false.

Compiling with npm run build then running npm start

> node --import=./tsnode.esm.js --enable-source-maps app.ts

(node:2688) ExperimentalWarning: Importing JSON modules is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
[DEV] Using ethereal mail transport
[DEV] Using ethereal mail transport
Error: Redis is already connecting/connected
    at /Users/aarhus/code/introdus/backend/node_modules/ioredis/built/Redis.js:107:24
    at new Promise (<anonymous>)
    at EventEmitter.connect (/Users/aarhus/code/introdus/backend/node_modules/ioredis/built/Redis.js:103:25)
    at <anonymous> (/Users/aarhus/code/introdus/backend/dist/src/common/helpers/db.js:32:6)
    at ModuleJob.run (node:internal/modules/esm/module_job:218:25)
    at async ModuleLoader.import (node:internal/modules/esm/loader:329:24)
    at async loadESM (node:internal/process/esm_loader:34:7)
    at async handleMainPromise (node:internal/modules/run_main:113:12)
Error: Redis is already connecting/connected
    at /Users/aarhus/code/introdus/backend/node_modules/ioredis/built/Redis.js:107:24
    at new Promise (<anonymous>)
    at EventEmitter.connect (/Users/aarhus/code/introdus/backend/node_modules/ioredis/built/Redis.js:103:25)
    at <anonymous> (/Users/aarhus/code/introdus/backend/src/common/helpers/db.ts:43:3)
    at ModuleJob.run (node:internal/modules/esm/module_job:218:25)
    at async ModuleLoader.import (node:internal/modules/esm/loader:329:24)
    at async loadESM (node:internal/process/esm_loader:34:7)
    at async handleMainPromise (node:internal/modules/run_main:113:12)
/Users/aarhus/code/introdus/backend/node_modules/mongoose/lib/index.js:587
      throw new _mongoose.Error.OverwriteModelError(name);
            ^

OverwriteModelError: Cannot overwrite `notificationSchema` model once compiled.
    at Mongoose.model (/Users/aarhus/code/introdus/backend/node_modules/mongoose/lib/index.js:587:13)
    at <anonymous> (/Users/aarhus/code/introdus/backend/src/common/helpers/mongooseModels.ts:3:39)
    at ModuleJob.run (node:internal/modules/esm/module_job:218:25)
    at async ModuleLoader.import (node:internal/modules/esm/loader:329:24)
    at async loadESM (node:internal/process/esm_loader:34:7)
    at async handleMainPromise (node:internal/modules/run_main:113:12)

Clean up base tsconfig in order to use ts-node

Cleaning up the tsconfig.json to the following settings:

{
	"$schema": "https://json.schemastore.org/tsconfig",
	"display": "Node 20",
	"compilerOptions": {

		/* Basic Options */
		"target": "ESNext",
		"module": "NodeNext",
		"lib": [
			"ESNext"
		],
		"allowJs": true /* Allow javascript files to be compiled. */,
		"declaration": true /* Generates corresponding '.d.ts' file. */,
		"sourceMap": true /* Generates corresponding '.map' file. */,
		"outDir": "dist" /* Redirect output structure to the directory. */,
		"removeComments": true /* Do not emit comments to output. */,
		"importHelpers": true /* Import emit helpers from 'tslib'. */,
		"isolatedModules": true,

		/* Strict Type-Checking Options */
		"strict": true,
		"noImplicitAny": true,
		"strictPropertyInitialization": true,
		"noImplicitThis": true,

		/* Additional Checks */
		"noUnusedLocals": true /* Report errors on unused locals. */,

		/* Module Resolution Options */
		"moduleResolution": "Node16",
		"baseUrl": "./",
		"paths": {
      "#src/*": ["src/*"],
			"#reports/*": ["reports/*"]
    },
		// "rootDirs": [],
		"typeRoots": [
			"./types"
		],
		"allowSyntheticDefaultImports": true,
		"esModuleInterop": true,
		"resolveJsonModule": true,

		/* Source Map Options */
		"sourceRoot": "./" 
		"inlineSources": true /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */,

		/* Experimental Options */
		"experimentalDecorators": true,
		"emitDecoratorMetadata": true,

		/* Advanced Options */
		"skipLibCheck": true,
		"forceConsistentCasingInFileNames": true
	},
	"ts-node": {
		"esm": true,
		"swc": true,
		"transpileOnly": true
	},

	"exclude": [
		"node_modules",
		"**/*.spec.ts",
		"dist",
		"logs",
		"app.js/",
		"gulpfile.js",
		"test"
	]
}

This gives the desired settings for module and moduleResolution making it seem like the most correct solution. This solution also attempts to use swc to compile files, with ts-node. However, this approach results in an error with .json imports, that my visual studio code is not showing. Only the compiler gives this error:

node:internal/process/esm_loader:40
      internalBinding('errors').triggerUncaughtException(
                                ^
TypeError [Error]: Module "file:///Users/aarhus/code/introdus/backend/src/common/auth/helpers/whitelist.json" needs an import attribute of type "json"
    at validateAttributes (node:internal/modules/esm/assert:89:15)
    at defaultLoad (node:internal/modules/esm/load:150:3)
    at async nextLoad (node:internal/modules/esm/hooks:865:22)
    at async /Users/aarhus/code/introdus/backend/node_modules/ts-node/src/esm.ts:255:39
    at async addShortCircuitFlag (/Users/aarhus/code/introdus/backend/node_modules/ts-node/src/esm.ts:409:15)
    at async nextLoad (node:internal/modules/esm/hooks:865:22)
    at async Hooks.load (node:internal/modules/esm/hooks:448:20)
    at async handleMessage (node:internal/modules/esm/worker:196:18) {
  code: 'ERR_IMPORT_ASSERTION_TYPE_MISSING'
}

However it is being much faster, displaying the error in about ~0.5s, which is much more aligned with the expected execution speed.

The above error is only produced, when using the following script to run the application: "start": "node --import=./tsnode.esm.js --enable-source-maps app.ts",. If I instead use the script node --import=ts-node/esm --watch ./app.ts, it produces another error:

(node:41864) ExperimentalWarning: Watch mode is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
TypeError: Unknown file extension ".ts" for /Users/aarhus/code/introdus/backend/app.ts
    at Object.getFileProtocolModuleFormat [as file:] (node:internal/modules/esm/get_format:160:9)
    at defaultGetFormat (node:internal/modules/esm/get_format:203:36)
    at defaultLoad (node:internal/modules/esm/load:141:22)
    at async ModuleLoader.load (node:internal/modules/esm/loader:409:7)
    at async ModuleLoader.moduleProvider (node:internal/modules/esm/loader:291:45)
    at async link (node:internal/modules/esm/module_job:76:21) {
  code: 'ERR_UNKNOWN_FILE_EXTENSION'
}
Failed running './app.ts'

Walking further down this line and fixing the JSON import errors, actually enabled me to boot the local server, using npm run start (see script above). Great!

However, typegoose is now throwing a ton of Setting "Mixed" for property "FastTrackConfiguration.language" errors that weren't previously there. These errors aren't supposed to show, but seems to be an issue when trying to set a typegoose property equal to a typescript type, instead of a native one (String, Boolean Number etc.).

Various

  • Running npm run start (after compiling) takes 60+ seconds before it does anything
  • tsconfig.json gives an error if module is set to ESNext. I'm only seeing this error, using Typescript 5.4.3, not my previous 5.1 version
    • However, if I change module to Node16, the JSON imports are flagged with an error that in order to have JSON imports, I must use nodenext.
  • After setting the above settings (module and moduleResolution to Node16) I'm getting errors when importing types, even from external libraries.
    • A first it was the following:
import { MessageTypes } from "../../message/message.config.js";
         ^
SyntaxError: The requested module '../../message/message.config.js' does not provide an export named 'MessageTypes'

but fixing that is giving me generic errors, like this one:

import { modelOptions, prop, Ref } from "@typegoose/typegoose";
                             ^
SyntaxError: The requested module '@typegoose/typegoose' does not provide an export named 'Ref'`
  • If I set moduleResolution to bundler, I'm getting an error on JSON imports: `Dynamic imports only support a second argument when the '--module' option is set to 'esnext', 'node16', or 'nodenext'.ts(1324)´.

Things to try

Lastly, its worth investigating why I wanted to transfer to ESM in the first place. One is, it is the where the ecosystem is moving, so may as well get ahead of the change. Another is that it (hopefully) can simplify the DX and the tooling used. Third is that by simplifying tools, we'll get better errors and more easily able to integrate with reporting tools such as Sentry (which has proved difficult in the past).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment