expensify-common 2.0.183 → 2.0.184

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/CLI.d.ts ADDED
@@ -0,0 +1,160 @@
1
+ import type { NonEmptyObject, NonEmptyTuple } from 'type-fest';
2
+ /**
3
+ * A base CLI arg has only a description, which we will use in the help/usage message (built-in to any CLI).
4
+ */
5
+ type CLIArg = {
6
+ description: string;
7
+ };
8
+ /**
9
+ * A boolean arg is characterized only by its presence or absence so has no other fields,
10
+ * but we'll create a type alias to clearly distinguish it from other argument types.
11
+ */
12
+ type BooleanArg = CLIArg;
13
+ /**
14
+ * Any other argument is provided raw in process.argv as a string.
15
+ * It can remain a string, or can be transformed into another type by a custom `parse` function.
16
+ * It can be optional (by providing a default) or required (no default value).
17
+ * It can also supersede other named arguments when provided.
18
+ */
19
+ type StringArg<T = unknown> = CLIArg & {
20
+ default?: T;
21
+ parse?: (val: string) => T;
22
+ supersedes?: string[];
23
+ required?: boolean;
24
+ };
25
+ /**
26
+ * A positional argument is just a string arg, but also must be assigned a name which we will eventually expose the CLI consumer.
27
+ * If `variadic` is true, this must be the last positional arg and it collects all remaining positional args into a string[].
28
+ */
29
+ type PositionalArg<T = unknown> = StringArg<T> & {
30
+ name: string;
31
+ variadic?: true;
32
+ };
33
+ /**
34
+ * This type represents the config for a CLI.
35
+ * The last positional arg can be marked `variadic: true` to collect all remaining positional args into a string[].
36
+ */
37
+ type CLIConfig = NonEmptyObject<{
38
+ /**
39
+ * Record of named flags that are fully characterized by their presence or absence (present=true,absent=false).
40
+ * @example `--verbose`
41
+ */
42
+ flags?: Record<string, BooleanArg>;
43
+ /**
44
+ * Record of named arguments that are represented by a key and a value.
45
+ * @example `--threads=8`
46
+ * @example `--name Rory`
47
+ */
48
+ namedArgs?: Record<string, StringArg>;
49
+ /**
50
+ * Tuple of positional args.
51
+ * @example `myScript.ts arg1 arg2 arg3`
52
+ */
53
+ positionalArgs?: NonEmptyTuple<PositionalArg>;
54
+ }>;
55
+ /**
56
+ * Record of flags to boolean after parsing.
57
+ */
58
+ type ParsedFlags<Flags extends CLIConfig['flags']> = {
59
+ [K in keyof NonNullable<Flags>]: boolean;
60
+ };
61
+ /**
62
+ * Utility type to infer the final value of a string param. Either:
63
+ * - it's a plain string, or
64
+ * - it has a parse function and the final value is inferred from the return type of that function
65
+ */
66
+ type InferStringArgParsedValue<T extends StringArg> = T extends {
67
+ parse: (val: string) => infer R;
68
+ } ? R : string;
69
+ /**
70
+ * Record of named args after parsing.
71
+ */
72
+ type ParsedNamedArgs<NamedArgs extends CLIConfig['namedArgs']> = {
73
+ [K in keyof NonNullable<NamedArgs>]: InferStringArgParsedValue<NonNullable<NamedArgs>[K]>;
74
+ };
75
+ /**
76
+ * Record of positional args after parsing.
77
+ * Variadic args are parsed as string[]; all others use InferStringArgParsedValue.
78
+ */
79
+ type ParsedPositionalArgs<PositionalArgs extends CLIConfig['positionalArgs']> = {
80
+ [K in NonNullable<PositionalArgs>[number] as K['name']]: K extends {
81
+ variadic: true;
82
+ } ? string[] : InferStringArgParsedValue<K>;
83
+ };
84
+ /**
85
+ * Utility to parse command-line arguments to a script.
86
+ *
87
+ * @example
88
+ * ```
89
+ * const cli = new CLI({
90
+ * flags: {
91
+ * verbose: {
92
+ * description: 'Enable verbose logging',
93
+ * },
94
+ * },
95
+ * namedArgs: {
96
+ * time: {
97
+ * description: 'Time of day to greet (morning or evening)',
98
+ * default: 'morning',
99
+ * parse: (val) => {
100
+ * if (val !== 'morning' && val !== 'evening') {
101
+ * throw new Error('Must be "morning" or "evening"');
102
+ * }
103
+ * return val as 'morning' | 'evening';
104
+ * },
105
+ * },
106
+ * },
107
+ * positionalArgs: [
108
+ * {
109
+ * name: 'firstName'
110
+ * description: 'First name to greet',
111
+ * },
112
+ * {
113
+ * name: 'lastName',
114
+ * description: 'Last name to greet',
115
+ * default: '',
116
+ * },
117
+ * ],
118
+ * });
119
+ *
120
+ * let fullName = cli.positionalArgs.firstName;
121
+ * if (cli.flags.verbose) {
122
+ * fullName += cli.positionalArgs.lastName;
123
+ * }
124
+ * console.log(fullName);
125
+ * console.log(cli.namedArgs.time);
126
+ * ```
127
+ */
128
+ /**
129
+ * Built-in flags that are always available on any CLI.
130
+ */
131
+ type BuiltInFlags = {
132
+ yes: boolean;
133
+ no: boolean;
134
+ help: boolean;
135
+ };
136
+ declare class CLI<TConfig extends CLIConfig> {
137
+ private readonly config;
138
+ /**
139
+ * Flags after parsing (includes built-in flags like --yes, --no, and --help).
140
+ */
141
+ readonly flags: ParsedFlags<TConfig['flags']> & BuiltInFlags;
142
+ /**
143
+ * Named args after parsing.
144
+ */
145
+ readonly namedArgs: ParsedNamedArgs<TConfig['namedArgs']>;
146
+ /**
147
+ * Positional args after parsing, collected into a record keyed by the name of each arg.
148
+ */
149
+ readonly positionalArgs: ParsedPositionalArgs<TConfig['positionalArgs']>;
150
+ constructor(config: TConfig);
151
+ private printHelp;
152
+ private static parseStringArg;
153
+ /**
154
+ * Prompts the user for confirmation and returns true if they confirm (y/yes), false otherwise.
155
+ * If --yes flag was passed, returns true immediately without prompting.
156
+ * If --no flag was passed, returns false immediately without prompting.
157
+ */
158
+ promptUserConfirmation(message: string): Promise<boolean>;
159
+ }
160
+ export default CLI;
package/dist/CLI.js ADDED
@@ -0,0 +1,282 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ /**
49
+ * This file contains a CLI utility class which can be used to declaratively implement a strongly-typed CLI.
50
+ * You provide a CLIConfig defining your arguments, then the class will handle parsing argv, type validation, error handling, and help messages.
51
+ */
52
+ const readline = __importStar(require("readline"));
53
+ const SafeString_1 = __importDefault(require("./SafeString"));
54
+ class CLI {
55
+ constructor(config) {
56
+ var _a, _b, _c, _d, _e, _f;
57
+ this.config = config;
58
+ const rawArgs = process.argv.slice(2);
59
+ // Initialize all flags to false by default (including built-in flags)
60
+ this.flags = Object.assign(Object.assign({}, Object.fromEntries(Object.keys((_a = config.flags) !== null && _a !== void 0 ? _a : {}).map((key) => [key, false]))), { yes: false, no: false, help: false });
61
+ try {
62
+ const parsedNamedArgs = {};
63
+ const parsedPositionalArgs = {};
64
+ const providedNamedArgs = new Set();
65
+ let positionalIndex = 0;
66
+ for (let i = 0; i < rawArgs.length; i++) {
67
+ const rawArg = rawArgs.at(i);
68
+ if (rawArg === undefined) {
69
+ continue;
70
+ }
71
+ if (rawArg.startsWith('--')) {
72
+ // Either a flag or a named param
73
+ const [rawArgName, rawArgValue] = rawArg.slice(2).split('=');
74
+ if (rawArgName in this.flags) {
75
+ // Arg is a flag
76
+ this.flags[rawArgName] = true;
77
+ }
78
+ else if (config.namedArgs && rawArgName in config.namedArgs) {
79
+ // Arg is a named arg
80
+ providedNamedArgs.add(rawArgName);
81
+ // Grab the value from the split token, otherwise go for the next token
82
+ let argValueBeforeParse = '';
83
+ if (rawArgValue) {
84
+ argValueBeforeParse = rawArgValue;
85
+ }
86
+ else {
87
+ argValueBeforeParse = (_b = rawArgs.at(++i)) !== null && _b !== void 0 ? _b : '';
88
+ if (!argValueBeforeParse || argValueBeforeParse.startsWith('--')) {
89
+ throw new Error(`Missing value for --${rawArgName}`);
90
+ }
91
+ }
92
+ const spec = config.namedArgs[rawArgName];
93
+ parsedNamedArgs[rawArgName] = CLI.parseStringArg(argValueBeforeParse, rawArgName, spec);
94
+ }
95
+ else {
96
+ console.error(`Unknown flag: --${rawArgName}`);
97
+ process.exit(1);
98
+ }
99
+ }
100
+ else {
101
+ // Arg is a positional arg
102
+ const spec = (_c = config.positionalArgs) === null || _c === void 0 ? void 0 : _c.at(positionalIndex);
103
+ if (spec === undefined) {
104
+ throw new Error(`Unexpected arg: ${rawArg}`);
105
+ }
106
+ if (spec.variadic) {
107
+ // Variadic: collect this and all remaining non-flag args into an array
108
+ const collected = [];
109
+ for (let j = i; j < rawArgs.length; j++) {
110
+ const remaining = rawArgs.at(j);
111
+ if (remaining === undefined || remaining.startsWith('--')) {
112
+ break;
113
+ }
114
+ collected.push(remaining);
115
+ }
116
+ parsedPositionalArgs[spec.name] = collected;
117
+ break;
118
+ }
119
+ parsedPositionalArgs[spec.name] = CLI.parseStringArg(rawArg, spec.name, spec);
120
+ positionalIndex++;
121
+ }
122
+ }
123
+ // Handle help command
124
+ if (this.flags.help) {
125
+ this.printHelp();
126
+ process.exit(0);
127
+ }
128
+ // Handle supersession logic
129
+ const supersededArgs = new Set();
130
+ for (const [name, spec] of Object.entries((_d = config.namedArgs) !== null && _d !== void 0 ? _d : {})) {
131
+ if (providedNamedArgs.has(name) && spec.supersedes) {
132
+ for (const supersededArg of spec.supersedes) {
133
+ supersededArgs.add(supersededArg);
134
+ if (providedNamedArgs.has(supersededArg)) {
135
+ console.warn(`⚠️ Warning: --${supersededArg} is superseded by --${name} and will be ignored.`);
136
+ }
137
+ }
138
+ }
139
+ }
140
+ // Validate that all required args are present, assign defaults where values are not parsed
141
+ for (const [name, spec] of Object.entries((_e = config.namedArgs) !== null && _e !== void 0 ? _e : {})) {
142
+ if (name in parsedNamedArgs) {
143
+ if (supersededArgs.has(name)) {
144
+ parsedNamedArgs[name] = undefined;
145
+ }
146
+ }
147
+ else if (supersededArgs.has(name)) {
148
+ // This arg was superseded, so don't require it and don't assign a default
149
+ continue;
150
+ }
151
+ else if (spec.default !== undefined) {
152
+ parsedNamedArgs[name] = spec.default;
153
+ }
154
+ else if (spec.required === false) {
155
+ // Explicitly marked as optional, leave undefined
156
+ continue;
157
+ }
158
+ else {
159
+ // Arguments without defaults are required by default (unless explicitly marked as optional)
160
+ throw new Error(`Missing required named argument --${name}`);
161
+ }
162
+ }
163
+ for (const spec of (_f = config.positionalArgs) !== null && _f !== void 0 ? _f : []) {
164
+ if (!(spec.name in parsedPositionalArgs)) {
165
+ if (spec.default !== undefined) {
166
+ parsedPositionalArgs[spec.name] = spec.default;
167
+ }
168
+ else if (spec.variadic) {
169
+ parsedPositionalArgs[spec.name] = [];
170
+ }
171
+ else {
172
+ throw new Error(`Missing required positional argument --${spec.name}`);
173
+ }
174
+ }
175
+ }
176
+ this.namedArgs = parsedNamedArgs;
177
+ this.positionalArgs = parsedPositionalArgs;
178
+ }
179
+ catch (err) {
180
+ // If help flag was set, the error is from process.exit(0) in tests (where it's mocked to throw) - just rethrow it
181
+ if (this.flags.help) {
182
+ throw err;
183
+ }
184
+ if (err instanceof Error) {
185
+ console.error(err.message);
186
+ this.printHelp();
187
+ }
188
+ else {
189
+ console.error('An unexpected error occurred initializing the CLI.');
190
+ }
191
+ process.exit(1);
192
+ }
193
+ }
194
+ printHelp() {
195
+ var _a;
196
+ const { flags = {}, namedArgs = {}, positionalArgs = [] } = this.config;
197
+ const scriptName = (_a = process.argv.at(1)) !== null && _a !== void 0 ? _a : 'script.ts';
198
+ const positionalUsage = positionalArgs
199
+ .map((arg) => {
200
+ const label = arg.variadic ? `${arg.name}...` : arg.name;
201
+ return arg.default === undefined ? `<${label}>` : `[${label}]`;
202
+ })
203
+ .join(' ');
204
+ const namedArgUsage = Object.keys(namedArgs)
205
+ .map((key) => `[--${key} <value>]`)
206
+ .join(' ');
207
+ const flagUsage = [...Object.keys(flags), '--yes', '--no', '--help'].map((key) => `[${key.startsWith('--') ? key : `--${key}`}]`).join(' ');
208
+ console.log(`\nUsage: npx ts-node ${scriptName} ${flagUsage} ${namedArgUsage} ${positionalUsage}\n`);
209
+ console.log('Flags:');
210
+ for (const [name, spec] of Object.entries(flags)) {
211
+ console.log(` --${name.padEnd(20)} ${spec.description}`);
212
+ }
213
+ // Built-in flags
214
+ console.log(` --${'yes'.padEnd(20)} Automatically answer "yes" to all confirmation prompts.`);
215
+ console.log(` --${'no'.padEnd(20)} Automatically answer "no" to all confirmation prompts.`);
216
+ console.log(` --${'help'.padEnd(20)} Show this help message.`);
217
+ console.log('');
218
+ if (Object.keys(namedArgs).length > 0) {
219
+ console.log('Named Arguments:');
220
+ for (const [name, spec] of Object.entries(namedArgs)) {
221
+ const defaultLabel = spec.default !== undefined ? ` (default: ${(0, SafeString_1.default)(spec.default)})` : '';
222
+ const supersededLabel = spec.supersedes && spec.supersedes.length > 0 ? ` (supersedes: ${spec.supersedes.join(', ')})` : '';
223
+ console.log(` --${name.padEnd(20)} ${spec.description}${defaultLabel}${supersededLabel}`);
224
+ }
225
+ console.log('');
226
+ }
227
+ if (positionalArgs.length > 0) {
228
+ console.log('Positional Arguments:');
229
+ for (const arg of positionalArgs) {
230
+ const defaultLabel = arg.default !== undefined ? ` (default: ${(0, SafeString_1.default)(arg.default)})` : '';
231
+ console.log(` ${arg.name.padEnd(22)} ${arg.description}${defaultLabel}`);
232
+ }
233
+ console.log('');
234
+ }
235
+ }
236
+ static parseStringArg(rawString, paramName, spec) {
237
+ if ('parse' in spec && !!spec.parse) {
238
+ try {
239
+ return spec.parse(rawString);
240
+ }
241
+ catch (error) {
242
+ let errorMessage = '';
243
+ if (error instanceof Error) {
244
+ errorMessage = error.message;
245
+ }
246
+ console.error(`Invalid value for --${paramName}: ${errorMessage}`);
247
+ process.exit(1);
248
+ }
249
+ }
250
+ else {
251
+ return rawString;
252
+ }
253
+ }
254
+ /**
255
+ * Prompts the user for confirmation and returns true if they confirm (y/yes), false otherwise.
256
+ * If --yes flag was passed, returns true immediately without prompting.
257
+ * If --no flag was passed, returns false immediately without prompting.
258
+ */
259
+ promptUserConfirmation(message) {
260
+ return __awaiter(this, void 0, void 0, function* () {
261
+ // Check for built-in flags first
262
+ if (this.flags.yes) {
263
+ return true;
264
+ }
265
+ if (this.flags.no) {
266
+ return false;
267
+ }
268
+ const rl = readline.createInterface({
269
+ input: process.stdin,
270
+ output: process.stdout,
271
+ });
272
+ return new Promise((resolve) => {
273
+ rl.question(message, (answer) => {
274
+ rl.close();
275
+ const normalizedAnswer = answer.trim().toLowerCase();
276
+ resolve(normalizedAnswer === 'y' || normalizedAnswer === 'yes');
277
+ });
278
+ });
279
+ });
280
+ }
281
+ }
282
+ exports.default = CLI;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * SafeString is a utility function that converts a value to a string.
3
+ * It handles the problematic case of plain objects by converting them to JSON.
4
+ * It helps with eslint rule https://typescript-eslint.io/rules/no-base-to-string
5
+ * @param value - The value to convert to a string.
6
+ * @returns The string representation of the value.
7
+ */
8
+ export default function SafeString(value: unknown): string;
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = SafeString;
4
+ /**
5
+ * SafeString is a utility function that converts a value to a string.
6
+ * It handles the problematic case of plain objects by converting them to JSON.
7
+ * It helps with eslint rule https://typescript-eslint.io/rules/no-base-to-string
8
+ * @param value - The value to convert to a string.
9
+ * @returns The string representation of the value.
10
+ */
11
+ function SafeString(value) {
12
+ if (value === undefined || value === null) {
13
+ return '';
14
+ }
15
+ // Handle primitives explicitly so the final fallback never receives an object.
16
+ const valueType = typeof value;
17
+ if (valueType === 'string') {
18
+ return value;
19
+ }
20
+ if (valueType === 'number' || valueType === 'boolean' || valueType === 'function' || valueType === 'bigint' || valueType === 'symbol') {
21
+ const primitive = value;
22
+ return String(primitive);
23
+ }
24
+ if (valueType === 'object') {
25
+ if (Array.isArray(value)) {
26
+ try {
27
+ return JSON.stringify(value);
28
+ }
29
+ catch (_a) {
30
+ return '[object Array]';
31
+ }
32
+ }
33
+ const obj = value;
34
+ const hasCustomToString = obj.toString && obj.toString !== Object.prototype.toString;
35
+ if (hasCustomToString) {
36
+ return obj.toString();
37
+ }
38
+ if (value instanceof Map) {
39
+ return '[object Map]';
40
+ }
41
+ if (value instanceof Set) {
42
+ return '[object Set]';
43
+ }
44
+ try {
45
+ return JSON.stringify(obj);
46
+ }
47
+ catch (_b) {
48
+ return '[object Object]';
49
+ }
50
+ }
51
+ return '';
52
+ }
package/dist/index.d.ts CHANGED
@@ -18,3 +18,5 @@ export { default as fastMerge } from './fastMerge';
18
18
  export { default as Str } from './str';
19
19
  export { default as TLD_REGEX } from './tlds';
20
20
  export { default as md5 } from './md5';
21
+ export { default as CLI } from './CLI';
22
+ export { default as SafeString } from './SafeString';
package/dist/index.js CHANGED
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.md5 = exports.TLD_REGEX = exports.Str = exports.fastMerge = exports.Url = exports.Templates = exports.ReportHistoryStore = exports.PubSub = exports.PageEvent = exports.Num = exports.Network = exports.Logger = exports.ExpensiMark = exports.Device = exports.LOGIN_PARTNER_DETAILS = exports.CredentialsWrapper = exports.Cookie = exports.PUBLIC_DOMAINS_SET = exports.UI = exports.CONST = exports.g_cloudFrontImg = exports.g_cloudFront = exports.BrowserDetect = exports.APIDeferred = exports.API = void 0;
39
+ exports.SafeString = exports.CLI = exports.md5 = exports.TLD_REGEX = exports.Str = exports.fastMerge = exports.Url = exports.Templates = exports.ReportHistoryStore = exports.PubSub = exports.PageEvent = exports.Num = exports.Network = exports.Logger = exports.ExpensiMark = exports.Device = exports.LOGIN_PARTNER_DETAILS = exports.CredentialsWrapper = exports.Cookie = exports.PUBLIC_DOMAINS_SET = exports.UI = exports.CONST = exports.g_cloudFrontImg = exports.g_cloudFront = exports.BrowserDetect = exports.APIDeferred = exports.API = void 0;
40
40
  var API_1 = require("./API");
41
41
  Object.defineProperty(exports, "API", { enumerable: true, get: function () { return __importDefault(API_1).default; } });
42
42
  var APIDeferred_1 = require("./APIDeferred");
@@ -80,3 +80,7 @@ var tlds_1 = require("./tlds");
80
80
  Object.defineProperty(exports, "TLD_REGEX", { enumerable: true, get: function () { return __importDefault(tlds_1).default; } });
81
81
  var md5_1 = require("./md5");
82
82
  Object.defineProperty(exports, "md5", { enumerable: true, get: function () { return __importDefault(md5_1).default; } });
83
+ var CLI_1 = require("./CLI");
84
+ Object.defineProperty(exports, "CLI", { enumerable: true, get: function () { return __importDefault(CLI_1).default; } });
85
+ var SafeString_1 = require("./SafeString");
86
+ Object.defineProperty(exports, "SafeString", { enumerable: true, get: function () { return __importDefault(SafeString_1).default; } });
package/dist/str.d.ts CHANGED
@@ -565,6 +565,37 @@ declare const Str: {
565
565
  * @returns True if is a domain account email, otherwise false.
566
566
  */
567
567
  isDomainEmail(email: string): boolean;
568
+ /**
569
+ * Find the minimum indentation of any line in the string,
570
+ * and remove that number of leading spaces from every line in the string.
571
+ *
572
+ * It also removes at most one leading newline, to reflect a common usage:
573
+ *
574
+ * ```
575
+ * Str.dedent(`
576
+ * const myIndentedStr = 'Hello, world!';
577
+ * console.log(myIndentedStr);
578
+ * `)
579
+ * ```
580
+ *
581
+ * This implementation assumes you'd want that to be:
582
+ *
583
+ * ```
584
+ * const myIndentedStr = 'Hello, world!';
585
+ * console.log(myIndentedStr);
586
+ *
587
+ * ```
588
+ *
589
+ * Rather than:
590
+ *
591
+ * ```
592
+ *
593
+ * const myIndentedStr = 'Hello, world!';
594
+ * console.log(myIndentedStr);
595
+ *
596
+ * ```
597
+ */
598
+ dedent(str: string): string;
568
599
  /**
569
600
  * Polyfill for String.prototype.replaceAll
570
601
  */
package/dist/str.js CHANGED
@@ -1014,6 +1014,56 @@ const Str = {
1014
1014
  isDomainEmail(email) {
1015
1015
  return this.startsWith(email, '+@');
1016
1016
  },
1017
+ /**
1018
+ * Find the minimum indentation of any line in the string,
1019
+ * and remove that number of leading spaces from every line in the string.
1020
+ *
1021
+ * It also removes at most one leading newline, to reflect a common usage:
1022
+ *
1023
+ * ```
1024
+ * Str.dedent(`
1025
+ * const myIndentedStr = 'Hello, world!';
1026
+ * console.log(myIndentedStr);
1027
+ * `)
1028
+ * ```
1029
+ *
1030
+ * This implementation assumes you'd want that to be:
1031
+ *
1032
+ * ```
1033
+ * const myIndentedStr = 'Hello, world!';
1034
+ * console.log(myIndentedStr);
1035
+ *
1036
+ * ```
1037
+ *
1038
+ * Rather than:
1039
+ *
1040
+ * ```
1041
+ *
1042
+ * const myIndentedStr = 'Hello, world!';
1043
+ * console.log(myIndentedStr);
1044
+ *
1045
+ * ```
1046
+ */
1047
+ dedent(str) {
1048
+ var _a, _b;
1049
+ // Remove at most one leading newline
1050
+ const stringWithoutLeadingNewlines = str.replaceAll(/^\r?\n/g, '');
1051
+ // Split string by remaining newlines
1052
+ const lines = stringWithoutLeadingNewlines.replaceAll('\r\n', '\n').split('\n');
1053
+ // Find the minimum indentation of non-empty lines
1054
+ let minIndent = Number.MAX_SAFE_INTEGER;
1055
+ for (const line of lines) {
1056
+ if (line.trim().length === 0) {
1057
+ continue;
1058
+ }
1059
+ const indentation = (_b = (_a = line.match(/^ */)) === null || _a === void 0 ? void 0 : _a[0].length) !== null && _b !== void 0 ? _b : 0;
1060
+ if (indentation < minIndent) {
1061
+ minIndent = indentation;
1062
+ }
1063
+ }
1064
+ // Remove the common indentation
1065
+ return lines.map((line) => line.slice(minIndent)).join('\n');
1066
+ },
1017
1067
  /**
1018
1068
  * Polyfill for String.prototype.replaceAll
1019
1069
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expensify-common",
3
- "version": "2.0.183",
3
+ "version": "2.0.184",
4
4
  "author": "Expensify, Inc.",
5
5
  "description": "Expensify libraries and components shared across different repos",
6
6
  "homepage": "https://expensify.com",
@@ -70,6 +70,7 @@
70
70
  "jest-environment-jsdom": "^29.7.0",
71
71
  "jit-grunt": "^0.10.0",
72
72
  "prettier": "^3.3.3",
73
+ "type-fest": "5.7.0",
73
74
  "typescript": "^5.7.2",
74
75
  "typescript-eslint": "^8.61.0"
75
76
  },