Convert the streaming server to ESM (#29389)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
		@@ -1,4 +1,8 @@
 | 
				
			|||||||
 | 
					/* eslint-disable import/no-commonjs */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// @ts-check
 | 
					// @ts-check
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// @ts-ignore - This needs to be a CJS file (eslint does not yet support ESM configs), and TS is complaining we use require
 | 
				
			||||||
const { defineConfig } = require('eslint-define-config');
 | 
					const { defineConfig } = require('eslint-define-config');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = defineConfig({
 | 
					module.exports = defineConfig({
 | 
				
			||||||
@@ -22,22 +26,18 @@ module.exports = defineConfig({
 | 
				
			|||||||
    // to maintain.
 | 
					    // to maintain.
 | 
				
			||||||
    'no-delete-var': 'off',
 | 
					    'no-delete-var': 'off',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // The streaming server is written in commonjs, not ESM for now:
 | 
					 | 
				
			||||||
    'import/no-commonjs': 'off',
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // This overrides the base configuration for this rule to pick up
 | 
					    // This overrides the base configuration for this rule to pick up
 | 
				
			||||||
    // dependencies for the streaming server from the correct package.json file.
 | 
					    // dependencies for the streaming server from the correct package.json file.
 | 
				
			||||||
    'import/no-extraneous-dependencies': [
 | 
					    'import/no-extraneous-dependencies': [
 | 
				
			||||||
      'error',
 | 
					      'error',
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        devDependencies: [
 | 
					        devDependencies: ['streaming/.eslintrc.cjs'],
 | 
				
			||||||
          'streaming/.eslintrc.js',
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
        optionalDependencies: false,
 | 
					        optionalDependencies: false,
 | 
				
			||||||
        peerDependencies: false,
 | 
					        peerDependencies: false,
 | 
				
			||||||
        includeTypes: true,
 | 
					        includeTypes: true,
 | 
				
			||||||
        packageDir: __dirname,
 | 
					        packageDir: __dirname,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
 | 
					    'import/extensions': ['error', 'always'],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
@@ -5,15 +5,14 @@
 | 
				
			|||||||
 * override it in let statements.
 | 
					 * override it in let statements.
 | 
				
			||||||
 * @type {string}
 | 
					 * @type {string}
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
const UNEXPECTED_ERROR_MESSAGE = 'An unexpected error occurred';
 | 
					export const UNEXPECTED_ERROR_MESSAGE = 'An unexpected error occurred';
 | 
				
			||||||
exports.UNKNOWN_ERROR_MESSAGE = UNEXPECTED_ERROR_MESSAGE;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Extracts the status and message properties from the error object, if
 | 
					 * Extracts the status and message properties from the error object, if
 | 
				
			||||||
 * available for public use. The `unknown` is for catch statements
 | 
					 * available for public use. The `unknown` is for catch statements
 | 
				
			||||||
 * @param {Error | AuthenticationError | RequestError | unknown} err
 | 
					 * @param {Error | AuthenticationError | RequestError | unknown} err
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
exports.extractStatusAndMessage = function(err) {
 | 
					export function extractStatusAndMessage(err) {
 | 
				
			||||||
  let statusCode = 500;
 | 
					  let statusCode = 500;
 | 
				
			||||||
  let errorMessage = UNEXPECTED_ERROR_MESSAGE;
 | 
					  let errorMessage = UNEXPECTED_ERROR_MESSAGE;
 | 
				
			||||||
  if (err instanceof AuthenticationError || err instanceof RequestError) {
 | 
					  if (err instanceof AuthenticationError || err instanceof RequestError) {
 | 
				
			||||||
@@ -22,9 +21,9 @@ exports.extractStatusAndMessage = function(err) {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return { statusCode, errorMessage };
 | 
					  return { statusCode, errorMessage };
 | 
				
			||||||
};
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RequestError extends Error {
 | 
					export class RequestError extends Error {
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * @param {string} message
 | 
					   * @param {string} message
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
@@ -35,9 +34,7 @@ class RequestError extends Error {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
exports.RequestError = RequestError;
 | 
					export class AuthenticationError extends Error {
 | 
				
			||||||
 | 
					 | 
				
			||||||
class AuthenticationError extends Error {
 | 
					 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * @param {string} message
 | 
					   * @param {string} message
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
@@ -47,5 +44,3 @@ class AuthenticationError extends Error {
 | 
				
			|||||||
    this.status = 401;
 | 
					    this.status = 401;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
exports.AuthenticationError = AuthenticationError;
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,32 +1,36 @@
 | 
				
			|||||||
// @ts-check
 | 
					// @ts-check
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const fs = require('fs');
 | 
					import fs from 'node:fs';
 | 
				
			||||||
const http = require('http');
 | 
					import http from 'node:http';
 | 
				
			||||||
const path = require('path');
 | 
					import path from 'node:path';
 | 
				
			||||||
const url = require('url');
 | 
					import url from 'node:url';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const cors = require('cors');
 | 
					import cors from 'cors';
 | 
				
			||||||
const dotenv = require('dotenv');
 | 
					import dotenv from 'dotenv';
 | 
				
			||||||
const express = require('express');
 | 
					import express from 'express';
 | 
				
			||||||
const { Redis } = require('ioredis');
 | 
					import { Redis } from 'ioredis';
 | 
				
			||||||
const { JSDOM } = require('jsdom');
 | 
					import { JSDOM } from 'jsdom';
 | 
				
			||||||
const pg = require('pg');
 | 
					import pg from 'pg';
 | 
				
			||||||
const dbUrlToConfig = require('pg-connection-string').parse;
 | 
					import pgConnectionString from 'pg-connection-string';
 | 
				
			||||||
const WebSocket = require('ws');
 | 
					import WebSocket from 'ws';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const errors = require('./errors');
 | 
					import { AuthenticationError, RequestError, extractStatusAndMessage as extractErrorStatusAndMessage } from './errors.js';
 | 
				
			||||||
const { AuthenticationError, RequestError } = require('./errors');
 | 
					import { logger, httpLogger, initializeLogLevel, attachWebsocketHttpLogger, createWebsocketLogger } from './logging.js';
 | 
				
			||||||
const { logger, httpLogger, initializeLogLevel, attachWebsocketHttpLogger, createWebsocketLogger } = require('./logging');
 | 
					import { setupMetrics } from './metrics.js';
 | 
				
			||||||
const { setupMetrics } = require('./metrics');
 | 
					import { isTruthy, normalizeHashtag, firstParam } from './utils.js';
 | 
				
			||||||
const { isTruthy, normalizeHashtag, firstParam } = require("./utils");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const environment = process.env.NODE_ENV || 'development';
 | 
					const environment = process.env.NODE_ENV || 'development';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Correctly detect and load .env or .env.production file based on environment:
 | 
					// Correctly detect and load .env or .env.production file based on environment:
 | 
				
			||||||
const dotenvFile = environment === 'production' ? '.env.production' : '.env';
 | 
					const dotenvFile = environment === 'production' ? '.env.production' : '.env';
 | 
				
			||||||
 | 
					const dotenvFilePath = path.resolve(
 | 
				
			||||||
 | 
					  url.fileURLToPath(
 | 
				
			||||||
 | 
					    new URL(path.join('..', dotenvFile), import.meta.url)
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dotenv.config({
 | 
					dotenv.config({
 | 
				
			||||||
  path: path.resolve(__dirname, path.join('..', dotenvFile))
 | 
					  path: dotenvFilePath
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
initializeLogLevel(process.env, environment);
 | 
					initializeLogLevel(process.env, environment);
 | 
				
			||||||
@@ -143,7 +147,7 @@ const pgConfigFromEnv = (env) => {
 | 
				
			|||||||
  let baseConfig = {};
 | 
					  let baseConfig = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (env.DATABASE_URL) {
 | 
					  if (env.DATABASE_URL) {
 | 
				
			||||||
    const parsedUrl = dbUrlToConfig(env.DATABASE_URL);
 | 
					    const parsedUrl = pgConnectionString.parse(env.DATABASE_URL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // The result of dbUrlToConfig from pg-connection-string is not type
 | 
					    // The result of dbUrlToConfig from pg-connection-string is not type
 | 
				
			||||||
    // compatible with pg.PoolConfig, since parts of the connection URL may be
 | 
					    // compatible with pg.PoolConfig, since parts of the connection URL may be
 | 
				
			||||||
@@ -326,7 +330,7 @@ const startServer = async () => {
 | 
				
			|||||||
      // Unfortunately for using the on('upgrade') setup, we need to manually
 | 
					      // Unfortunately for using the on('upgrade') setup, we need to manually
 | 
				
			||||||
      // write a HTTP Response to the Socket to close the connection upgrade
 | 
					      // write a HTTP Response to the Socket to close the connection upgrade
 | 
				
			||||||
      // attempt, so the following code is to handle all of that.
 | 
					      // attempt, so the following code is to handle all of that.
 | 
				
			||||||
      const {statusCode, errorMessage } = errors.extractStatusAndMessage(err);
 | 
					      const {statusCode, errorMessage } = extractErrorStatusAndMessage(err);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      /** @type {Record<string, string | number | import('pino-http').ReqId>} */
 | 
					      /** @type {Record<string, string | number | import('pino-http').ReqId>} */
 | 
				
			||||||
      const headers = {
 | 
					      const headers = {
 | 
				
			||||||
@@ -748,7 +752,7 @@ const startServer = async () => {
 | 
				
			|||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const {statusCode, errorMessage } = errors.extractStatusAndMessage(err);
 | 
					    const {statusCode, errorMessage } = extractErrorStatusAndMessage(err);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.writeHead(statusCode, { 'Content-Type': 'application/json' });
 | 
					    res.writeHead(statusCode, { 'Content-Type': 'application/json' });
 | 
				
			||||||
    res.end(JSON.stringify({ error: errorMessage }));
 | 
					    res.end(JSON.stringify({ error: errorMessage }));
 | 
				
			||||||
@@ -1155,7 +1159,7 @@ const startServer = async () => {
 | 
				
			|||||||
      // @ts-ignore
 | 
					      // @ts-ignore
 | 
				
			||||||
      streamFrom(channelIds, req, req.log, onSend, onEnd, 'eventsource', options.needsFiltering);
 | 
					      streamFrom(channelIds, req, req.log, onSend, onEnd, 'eventsource', options.needsFiltering);
 | 
				
			||||||
    }).catch(err => {
 | 
					    }).catch(err => {
 | 
				
			||||||
      const {statusCode, errorMessage } = errors.extractStatusAndMessage(err);
 | 
					      const {statusCode, errorMessage } = extractErrorStatusAndMessage(err);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      res.log.info({ err }, 'Eventsource subscription error');
 | 
					      res.log.info({ err }, 'Eventsource subscription error');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1353,7 +1357,7 @@ const startServer = async () => {
 | 
				
			|||||||
        stopHeartbeat,
 | 
					        stopHeartbeat,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
    }).catch(err => {
 | 
					    }).catch(err => {
 | 
				
			||||||
      const {statusCode, errorMessage } = errors.extractStatusAndMessage(err);
 | 
					      const {statusCode, errorMessage } = extractErrorStatusAndMessage(err);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      logger.error({ err }, 'Websocket subscription error');
 | 
					      logger.error({ err }, 'Websocket subscription error');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1482,13 +1486,15 @@ const startServer = async () => {
 | 
				
			|||||||
      // Decrement the metrics for connected clients:
 | 
					      // Decrement the metrics for connected clients:
 | 
				
			||||||
      connectedClients.labels({ type: 'websocket' }).dec();
 | 
					      connectedClients.labels({ type: 'websocket' }).dec();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // We need to delete the session object as to ensure it correctly gets
 | 
					      // We need to unassign the session object as to ensure it correctly gets
 | 
				
			||||||
      // garbage collected, without doing this we could accidentally hold on to
 | 
					      // garbage collected, without doing this we could accidentally hold on to
 | 
				
			||||||
      // references to the websocket, the request, and the logger, causing
 | 
					      // references to the websocket, the request, and the logger, causing
 | 
				
			||||||
      // memory leaks.
 | 
					      // memory leaks.
 | 
				
			||||||
      //
 | 
					
 | 
				
			||||||
      // @ts-ignore
 | 
					      // This is commented out because `delete` only operated on object properties
 | 
				
			||||||
      delete session;
 | 
					      // It needs to be replaced by `session = undefined`, but it requires every calls to
 | 
				
			||||||
 | 
					      // `session` to check for it, thus a significant refactor
 | 
				
			||||||
 | 
					      // delete session;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Note: immediately after the `error` event is emitted, the `close` event
 | 
					    // Note: immediately after the `error` event is emitted, the `close` event
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
const { pino } = require('pino');
 | 
					import { pino } from 'pino';
 | 
				
			||||||
const { pinoHttp, stdSerializers: pinoHttpSerializers } = require('pino-http');
 | 
					import { pinoHttp, stdSerializers as pinoHttpSerializers } from 'pino-http';
 | 
				
			||||||
const uuid = require('uuid');
 | 
					import * as uuid from 'uuid';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Generates the Request ID for logging and setting on responses
 | 
					 * Generates the Request ID for logging and setting on responses
 | 
				
			||||||
@@ -36,7 +36,7 @@ function sanitizeRequestLog(req) {
 | 
				
			|||||||
  return log;
 | 
					  return log;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const logger = pino({
 | 
					export const logger = pino({
 | 
				
			||||||
  name: "streaming",
 | 
					  name: "streaming",
 | 
				
			||||||
  // Reformat the log level to a string:
 | 
					  // Reformat the log level to a string:
 | 
				
			||||||
  formatters: {
 | 
					  formatters: {
 | 
				
			||||||
@@ -59,7 +59,7 @@ const logger = pino({
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const httpLogger = pinoHttp({
 | 
					export const httpLogger = pinoHttp({
 | 
				
			||||||
  logger,
 | 
					  logger,
 | 
				
			||||||
  genReqId: generateRequestId,
 | 
					  genReqId: generateRequestId,
 | 
				
			||||||
  serializers: {
 | 
					  serializers: {
 | 
				
			||||||
@@ -71,7 +71,7 @@ const httpLogger = pinoHttp({
 | 
				
			|||||||
 * Attaches a logger to the request object received by http upgrade handlers
 | 
					 * Attaches a logger to the request object received by http upgrade handlers
 | 
				
			||||||
 * @param {http.IncomingMessage} request
 | 
					 * @param {http.IncomingMessage} request
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function attachWebsocketHttpLogger(request) {
 | 
					export function attachWebsocketHttpLogger(request) {
 | 
				
			||||||
  generateRequestId(request);
 | 
					  generateRequestId(request);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  request.log = logger.child({
 | 
					  request.log = logger.child({
 | 
				
			||||||
@@ -84,7 +84,7 @@ function attachWebsocketHttpLogger(request) {
 | 
				
			|||||||
 * @param {http.IncomingMessage} request
 | 
					 * @param {http.IncomingMessage} request
 | 
				
			||||||
 * @param {import('./index.js').ResolvedAccount} resolvedAccount
 | 
					 * @param {import('./index.js').ResolvedAccount} resolvedAccount
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function createWebsocketLogger(request, resolvedAccount) {
 | 
					export function createWebsocketLogger(request, resolvedAccount) {
 | 
				
			||||||
  // ensure the request.id is always present.
 | 
					  // ensure the request.id is always present.
 | 
				
			||||||
  generateRequestId(request);
 | 
					  generateRequestId(request);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -98,17 +98,12 @@ function createWebsocketLogger(request, resolvedAccount) {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
exports.logger = logger;
 | 
					 | 
				
			||||||
exports.httpLogger = httpLogger;
 | 
					 | 
				
			||||||
exports.attachWebsocketHttpLogger = attachWebsocketHttpLogger;
 | 
					 | 
				
			||||||
exports.createWebsocketLogger = createWebsocketLogger;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Initializes the log level based on the environment
 | 
					 * Initializes the log level based on the environment
 | 
				
			||||||
 * @param {Object<string, any>} env
 | 
					 * @param {Object<string, any>} env
 | 
				
			||||||
 * @param {string} environment
 | 
					 * @param {string} environment
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
exports.initializeLogLevel = function initializeLogLevel(env, environment) {
 | 
					export function initializeLogLevel(env, environment) {
 | 
				
			||||||
  if (env.LOG_LEVEL && Object.keys(logger.levels.values).includes(env.LOG_LEVEL)) {
 | 
					  if (env.LOG_LEVEL && Object.keys(logger.levels.values).includes(env.LOG_LEVEL)) {
 | 
				
			||||||
    logger.level = env.LOG_LEVEL;
 | 
					    logger.level = env.LOG_LEVEL;
 | 
				
			||||||
  } else if (environment === 'development') {
 | 
					  } else if (environment === 'development') {
 | 
				
			||||||
@@ -116,4 +111,4 @@ exports.initializeLogLevel = function initializeLogLevel(env, environment) {
 | 
				
			|||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    logger.level = 'info';
 | 
					    logger.level = 'info';
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
// @ts-check
 | 
					// @ts-check
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const metrics = require('prom-client');
 | 
					import metrics from 'prom-client';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @typedef StreamingMetrics
 | 
					 * @typedef StreamingMetrics
 | 
				
			||||||
@@ -18,7 +18,7 @@ const metrics = require('prom-client');
 | 
				
			|||||||
 * @param {import('pg').Pool} pgPool
 | 
					 * @param {import('pg').Pool} pgPool
 | 
				
			||||||
 * @returns {StreamingMetrics}
 | 
					 * @returns {StreamingMetrics}
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function setupMetrics(channels, pgPool) {
 | 
					export function setupMetrics(channels, pgPool) {
 | 
				
			||||||
  // Collect metrics from Node.js
 | 
					  // Collect metrics from Node.js
 | 
				
			||||||
  metrics.collectDefaultMetrics();
 | 
					  metrics.collectDefaultMetrics();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -101,5 +101,3 @@ function setupMetrics(channels, pgPool) {
 | 
				
			|||||||
    messagesSent,
 | 
					    messagesSent,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
exports.setupMetrics = setupMetrics;
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,7 @@
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  "description": "Mastodon's Streaming Server",
 | 
					  "description": "Mastodon's Streaming Server",
 | 
				
			||||||
  "private": true,
 | 
					  "private": true,
 | 
				
			||||||
 | 
					  "type": "module",
 | 
				
			||||||
  "repository": {
 | 
					  "repository": {
 | 
				
			||||||
    "type": "git",
 | 
					    "type": "git",
 | 
				
			||||||
    "url": "https://github.com/mastodon/mastodon.git"
 | 
					    "url": "https://github.com/mastodon/mastodon.git"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,11 +2,11 @@
 | 
				
			|||||||
  "extends": "../tsconfig.json",
 | 
					  "extends": "../tsconfig.json",
 | 
				
			||||||
  "compilerOptions": {
 | 
					  "compilerOptions": {
 | 
				
			||||||
    "target": "esnext",
 | 
					    "target": "esnext",
 | 
				
			||||||
    "module": "CommonJS",
 | 
					    "module": "NodeNext",
 | 
				
			||||||
    "moduleResolution": "node",
 | 
					    "moduleResolution": "NodeNext",
 | 
				
			||||||
    "noUnusedParameters": false,
 | 
					    "noUnusedParameters": false,
 | 
				
			||||||
    "tsBuildInfoFile": "../tmp/cache/streaming/tsconfig.tsbuildinfo",
 | 
					    "tsBuildInfoFile": "../tmp/cache/streaming/tsconfig.tsbuildinfo",
 | 
				
			||||||
    "paths": {},
 | 
					    "paths": {},
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "include": ["./*.js", "./.eslintrc.js"],
 | 
					  "include": ["./*.js", "./.eslintrc.cjs"],
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,11 +16,9 @@ const FALSE_VALUES = [
 | 
				
			|||||||
 * @param {any} value
 | 
					 * @param {any} value
 | 
				
			||||||
 * @returns {boolean}
 | 
					 * @returns {boolean}
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
const isTruthy = value =>
 | 
					export function isTruthy(value) {
 | 
				
			||||||
  value && !FALSE_VALUES.includes(value);
 | 
					  return value && !FALSE_VALUES.includes(value);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
exports.isTruthy = isTruthy;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * See app/lib/ascii_folder.rb for the canon definitions
 | 
					 * See app/lib/ascii_folder.rb for the canon definitions
 | 
				
			||||||
@@ -33,7 +31,7 @@ const EQUIVALENT_ASCII_CHARS = 'AAAAAAaaaaaaAaAaAaCcCcCcCcCcDdDdDdEEEEeeeeEeEeEe
 | 
				
			|||||||
 * @param {string} str
 | 
					 * @param {string} str
 | 
				
			||||||
 * @returns {string}
 | 
					 * @returns {string}
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function foldToASCII(str) {
 | 
					export function foldToASCII(str) {
 | 
				
			||||||
  const regex = new RegExp(NON_ASCII_CHARS.split('').join('|'), 'g');
 | 
					  const regex = new RegExp(NON_ASCII_CHARS.split('').join('|'), 'g');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return str.replace(regex, function(match) {
 | 
					  return str.replace(regex, function(match) {
 | 
				
			||||||
@@ -42,28 +40,22 @@ function foldToASCII(str) {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
exports.foldToASCII = foldToASCII;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @param {string} str
 | 
					 * @param {string} str
 | 
				
			||||||
 * @returns {string}
 | 
					 * @returns {string}
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function normalizeHashtag(str) {
 | 
					export function normalizeHashtag(str) {
 | 
				
			||||||
  return foldToASCII(str.normalize('NFKC').toLowerCase()).replace(/[^\p{L}\p{N}_\u00b7\u200c]/gu, '');
 | 
					  return foldToASCII(str.normalize('NFKC').toLowerCase()).replace(/[^\p{L}\p{N}_\u00b7\u200c]/gu, '');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
exports.normalizeHashtag = normalizeHashtag;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @param {string|string[]} arrayOrString
 | 
					 * @param {string|string[]} arrayOrString
 | 
				
			||||||
 * @returns {string}
 | 
					 * @returns {string}
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function firstParam(arrayOrString) {
 | 
					export function firstParam(arrayOrString) {
 | 
				
			||||||
  if (Array.isArray(arrayOrString)) {
 | 
					  if (Array.isArray(arrayOrString)) {
 | 
				
			||||||
    return arrayOrString[0];
 | 
					    return arrayOrString[0];
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    return arrayOrString;
 | 
					    return arrayOrString;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
exports.firstParam = firstParam;
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user