🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@amritk/helpers

Package Overview
Dependencies
Maintainers
1
Versions
19
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@amritk/helpers - npm Package Compare versions

Comparing version
0.10.3
to
0.11.0
+14
dist/quote-js-string.d.ts
/**
* Emits `text` as a double-quoted JS string literal for generated code.
*
* This is the one escape-or-quote decision for schema-controlled text
* (property names, patterns, enum values) embedded in generated source — a key
* like `it's` or a pattern with a quote or newline must never break out of the
* literal or inject code, so anything carrying such a character goes through
* `JSON.stringify`. The common all-plain string skips the full escaper: code
* generators emit one literal per assertion/message, and the stringify calls
* were a measurable slice of generation time. Centralized so the next
* generator wanting the fast path cannot get the security-sensitive regex
* subtly wrong.
*/
export declare const quoteJsString: (text: string) => string;
// Characters that force the JSON.stringify escaping path below: quote,
// backslash, C0 controls, and the JS line separators. Anything else sits
// verbatim inside a double-quoted literal.
// biome-ignore lint/suspicious/noControlCharactersInRegex: the C0 range is exactly the set that must never appear raw in a generated string literal
const NEEDS_ESCAPING = /["\\\u0000-\u001f\u2028\u2029]/;
/**
* Emits `text` as a double-quoted JS string literal for generated code.
*
* This is the one escape-or-quote decision for schema-controlled text
* (property names, patterns, enum values) embedded in generated source — a key
* like `it's` or a pattern with a quote or newline must never break out of the
* literal or inject code, so anything carrying such a character goes through
* `JSON.stringify`. The common all-plain string skips the full escaper: code
* generators emit one literal per assertion/message, and the stringify calls
* were a measurable slice of generation time. Centralized so the next
* generator wanting the fast path cannot get the security-sensitive regex
* subtly wrong.
*/
export const quoteJsString = (text) => (NEEDS_ESCAPING.test(text) ? JSON.stringify(text) : `"${text}"`);
// Characters that force the JSON.stringify escaping path below: quote,
// backslash, C0 controls, and the JS line separators. Anything else sits
// verbatim inside a double-quoted literal.
// biome-ignore lint/suspicious/noControlCharactersInRegex: the C0 range is exactly the set that must never appear raw in a generated string literal
const NEEDS_ESCAPING = /["\\\u0000-\u001f\u2028\u2029]/
/**
* Emits `text` as a double-quoted JS string literal for generated code.
*
* This is the one escape-or-quote decision for schema-controlled text
* (property names, patterns, enum values) embedded in generated source — a key
* like `it's` or a pattern with a quote or newline must never break out of the
* literal or inject code, so anything carrying such a character goes through
* `JSON.stringify`. The common all-plain string skips the full escaper: code
* generators emit one literal per assertion/message, and the stringify calls
* were a measurable slice of generation time. Centralized so the next
* generator wanting the fast path cannot get the security-sensitive regex
* subtly wrong.
*/
export const quoteJsString = (text: string): string => (NEEDS_ESCAPING.test(text) ? JSON.stringify(text) : `"${text}"`)
+0
-25

@@ -1,26 +0,1 @@

/**
* Escapes a JSON Schema `pattern` so it can be embedded between the slashes of
* a generated regex literal (`/…/`), and validates that the pattern is a legal
* regex at generation time.
*
* A `pattern` is an ECMA-262 regex *body*, and the generated text goes into a
* regex literal — not a string literal — so backslashes are regex syntax and
* must be left exactly as-is (doubling `\d` to `\\d` would change it from "a
* digit" to "a literal backslash followed by d"). Two things would otherwise
* corrupt the surrounding literal:
* - an *unescaped* `/`, which would close the literal early; and
* - a raw line terminator (LF, CR, U+2028, U+2029), which is not allowed
* inside a regex literal and would split the emitted `/…/` across lines (a
* syntax error). Each is rewritten to its backslash escape, which matches
* the identical character, so the regex's meaning is unchanged.
*
* Additionally, an *invalid* pattern (e.g. `([`) would be emitted verbatim as
* `/([/…` and produce output that does not parse. We compile the pattern with
* `new RegExp` here and throw a clear generator-time error instead, so a bad
* schema fails loudly during generation rather than emitting broken code.
*
* @example
* escapeRegexPattern('\\d{4}/\\d{2}') // → '\\d{4}\\/\\d{2}' (i.e. \d{4}\/\d{2})
* @throws if `pattern` is not a valid regular expression.
*/
export declare const escapeRegexPattern: (pattern: string) => string;
+26
-9

@@ -26,3 +26,20 @@ /**

*/
// Line terminators, keyed by code point, and the backslash escape each maps to.
// These are the four characters disallowed *raw* inside a regex literal.
const lineTerminatorEscapes = {
10: '\\n', // LF
13: '\\r', // CR
8232: '\\u2028', // LINE SEPARATOR
8233: '\\u2029', // PARAGRAPH SEPARATOR
};
// Memoized results: the same schema pattern is escaped once per parser, per
// validator, and per assertion context, on every generation — and the dominant
// cost is the validating `new RegExp` compile. Schemas carry a bounded set of
// patterns; the cap only guards a pathological long-lived process.
const escapeCache = new Map();
const ESCAPE_CACHE_LIMIT = 1000;
export const escapeRegexPattern = (pattern) => {
const cached = escapeCache.get(pattern);
if (cached !== undefined)
return cached;
// Validate at generation time — an invalid pattern must fail here, not emit

@@ -36,10 +53,2 @@ // a `/([/` literal that breaks the generated file.

}
// Line terminators, keyed by code point, and the backslash escape each maps to.
// These are the four characters disallowed *raw* inside a regex literal.
const lineTerminatorEscapes = {
10: '\\n', // LF
13: '\\r', // CR
8232: '\\u2028', // LINE SEPARATOR
8233: '\\u2029', // PARAGRAPH SEPARATOR
};
// Match either an escape sequence (`\` + any char, kept verbatim) or a single

@@ -51,3 +60,3 @@ // character that would corrupt the literal (a bare `/` or a line terminator).

// double-escaped.
return pattern.replace(/\\[\s\S]|[/\n\r\u2028\u2029]/g, (match) => {
const escaped = pattern.replace(/\\[\s\S]|[/\n\r\u2028\u2029]/g, (match) => {
if (match === '/')

@@ -61,2 +70,10 @@ return '\\/';

});
if (escapeCache.size >= ESCAPE_CACHE_LIMIT) {
// Evict the single oldest entry (Maps iterate in insertion order) instead
// of clearing wholesale — a corpus slightly over the limit keeps its hot
// set instead of collapsing to a 0% hit rate every pass.
escapeCache.delete(escapeCache.keys().next().value);
}
escapeCache.set(pattern, escaped);
return escaped;
};

@@ -14,2 +14,9 @@ /** A generated file: its name (with extension) and TypeScript source. */

readonly typesOnly?: boolean;
/**
* Extension used on every relative re-export specifier. `'js'` (default) is
* the standard TS NodeNext form (`./x.js` resolving to a sibling `x.ts`);
* `'ts'` emits the literal on-disk path so the output runs directly under
* Node's type stripping.
*/
readonly importExt?: 'js' | 'ts';
};

@@ -16,0 +23,0 @@ /**

@@ -1,6 +0,56 @@

// Generated files declare their public surface with these two forms, so we can
// recover the export names from the source text without parsing it.
const TYPE_EXPORT_RE = /^export type (\w+)/gm;
const CONST_EXPORT_RE = /^export const (\w+)/gm;
// Generated files declare their public surface with `export type <Name>` /
// `export const <Name>` at line starts, so the names can be recovered from the
// source text without parsing it.
/** Reads the identifier following `prefix` when `content` starts with it at `at`. */
const exportNameAt = (content, at, prefix) => {
if (!content.startsWith(prefix, at))
return null;
let end = at + prefix.length;
while (end < content.length) {
const code = content.charCodeAt(end);
const isWord = (code >= 97 && code <= 122) || (code >= 65 && code <= 90) || (code >= 48 && code <= 57) || code === 95;
if (!isWord)
break;
end++;
}
return end > at + prefix.length ? content.slice(at + prefix.length, end) : null;
};
/** True for every JS LineTerminator code unit — the same set the old `/m` regexes anchored after. */
const isLineTerminator = (code) => code === 10 || code === 13 || code === 8232 || code === 8233;
/**
* Collects `export type` / `export const` names with a single line-start walk.
* A multiline-anchored regex scan (`/^export .../gm`) does the same job but
* showed up at several percent of total generation time in CPU profiles — the
* regex engine re-anchors at every line of every generated file on every build.
*/
const collectExportNames = (content, typeNames, constNames) => {
let lineStart = 0;
while (lineStart < content.length) {
// charCode prefilter: almost every line starts with whitespace, a brace, or
// a keyword other than `export` — one integer compare skips the substring
// comparison for all of them (101 === 'e').
if (content.charCodeAt(lineStart) === 101 && content.startsWith('export ', lineStart)) {
const typeName = exportNameAt(content, lineStart, 'export type ');
if (typeName !== null) {
typeNames.push(typeName);
}
else {
const constName = exportNameAt(content, lineStart, 'export const ');
if (constName !== null)
constNames.push(constName);
}
}
// Advance past the next line break of ANY JS flavor (LF, CR, CRLF,
// U+2028, U+2029) — matching the multiline regexes this walk replaced,
// which treated all of them as line starts.
let next = lineStart;
while (next < content.length && !isLineTerminator(content.charCodeAt(next)))
next++;
if (next >= content.length)
break;
// \r\n counts as one break.
lineStart = content.charCodeAt(next) === 13 && content.charCodeAt(next + 1) === 10 ? next + 2 : next + 1;
}
};
/**
* Builds the `index.ts` barrel that re-exports every generated module. This is

@@ -20,2 +70,3 @@ * the shared version of the near-identical barrel each generator used to build

const typesOnly = options.typesOnly ?? false;
const importExt = options.importExt ?? 'js';
const sortedFiles = files

@@ -29,15 +80,12 @@ .filter((file) => !file.filename.startsWith('_helpers/'))

const constNames = [];
for (const match of file.content.matchAll(TYPE_EXPORT_RE))
typeNames.push(match[1]);
for (const match of file.content.matchAll(CONST_EXPORT_RE))
constNames.push(match[1]);
collectExportNames(file.content, typeNames, constNames);
if (typeNames.length === 0 && constNames.length === 0)
continue;
// `.js` extension so the barrel resolves under Node ESM, not only Bun.
// An explicit extension so the barrel resolves under Node ESM, not only Bun.
if (typesOnly) {
indexContent += `export type { ${typeNames.join(', ')} } from './${moduleName}.js';\n`;
indexContent += `export type { ${typeNames.join(', ')} } from './${moduleName}.${importExt}';\n`;
}
else {
const typeExports = typeNames.map((name) => `type ${name}`);
indexContent += `export { ${[...typeExports, ...constNames].join(', ')} } from './${moduleName}.js';\n`;
indexContent += `export { ${[...typeExports, ...constNames].join(', ')} } from './${moduleName}.${importExt}';\n`;
}

@@ -44,0 +92,0 @@ }

@@ -1,2 +0,13 @@

/** Parses the items of an array with a parser function */
/**
* Parses the items of an array with a parser function.
*
* When every element parses to the very same reference — the parser found it
* already valid and handed it back, as the generated item sub-parsers do for
* clean values — the input array itself is returned and nothing is allocated.
* The result array is only materialized (lazily, on the first element the
* parser actually replaced) when something was coerced or stripped, so the
* common all-clean parse costs no allocation at all. Callers already share
* element references either way; sharing the container mirrors the generated
* fast paths, which return the input array by reference too.
*/
export declare const validateArray: (input: unknown, parser: (input: unknown) => unknown) => any[];

@@ -1,2 +0,13 @@

/** Parses the items of an array with a parser function */
/**
* Parses the items of an array with a parser function.
*
* When every element parses to the very same reference — the parser found it
* already valid and handed it back, as the generated item sub-parsers do for
* clean values — the input array itself is returned and nothing is allocated.
* The result array is only materialized (lazily, on the first element the
* parser actually replaced) when something was coerced or stripped, so the
* common all-clean parse costs no allocation at all. Callers already share
* element references either way; sharing the container mirrors the generated
* fast paths, which return the input array by reference too.
*/
export const validateArray = (input, parser) => {

@@ -6,9 +17,18 @@ if (!Array.isArray(input)) {

}
// Pre-allocate the result array for better performance than push()
const len = input.length;
const result = new Array(len);
let result = null;
for (let i = 0; i < len; i++) {
result[i] = parser(input[i]);
const parsed = parser(input[i]);
if (result !== null) {
result[i] = parsed;
}
else if (parsed !== input[i]) {
// First replaced element: materialize the copy and backfill the prefix.
result = new Array(len);
for (let j = 0; j < i; j++)
result[j] = input[j];
result[i] = parsed;
}
}
return result;
return result ?? input;
};
{
"name": "@amritk/helpers",
"version": "0.10.3",
"version": "0.11.0",
"description": "Shared utilities for the mjst code generation ecosystem.",

@@ -5,0 +5,0 @@ "type": "module",

@@ -26,3 +26,22 @@ /**

*/
// Line terminators, keyed by code point, and the backslash escape each maps to.
// These are the four characters disallowed *raw* inside a regex literal.
const lineTerminatorEscapes: Record<number, string> = {
10: '\\n', // LF
13: '\\r', // CR
8232: '\\u2028', // LINE SEPARATOR
8233: '\\u2029', // PARAGRAPH SEPARATOR
}
// Memoized results: the same schema pattern is escaped once per parser, per
// validator, and per assertion context, on every generation — and the dominant
// cost is the validating `new RegExp` compile. Schemas carry a bounded set of
// patterns; the cap only guards a pathological long-lived process.
const escapeCache = new Map<string, string>()
const ESCAPE_CACHE_LIMIT = 1000
export const escapeRegexPattern = (pattern: string): string => {
const cached = escapeCache.get(pattern)
if (cached !== undefined) return cached
// Validate at generation time — an invalid pattern must fail here, not emit

@@ -38,11 +57,2 @@ // a `/([/` literal that breaks the generated file.

// Line terminators, keyed by code point, and the backslash escape each maps to.
// These are the four characters disallowed *raw* inside a regex literal.
const lineTerminatorEscapes: Record<number, string> = {
10: '\\n', // LF
13: '\\r', // CR
8232: '\\u2028', // LINE SEPARATOR
8233: '\\u2029', // PARAGRAPH SEPARATOR
}
// Match either an escape sequence (`\` + any char, kept verbatim) or a single

@@ -54,3 +64,3 @@ // character that would corrupt the literal (a bare `/` or a line terminator).

// double-escaped.
return pattern.replace(/\\[\s\S]|[/\n\r\u2028\u2029]/g, (match) => {
const escaped = pattern.replace(/\\[\s\S]|[/\n\r\u2028\u2029]/g, (match) => {
if (match === '/') return '\\/'

@@ -62,2 +72,11 @@ const terminatorEscape = lineTerminatorEscapes[match.charCodeAt(0)]

})
if (escapeCache.size >= ESCAPE_CACHE_LIMIT) {
// Evict the single oldest entry (Maps iterate in insertion order) instead
// of clearing wholesale — a corpus slightly over the limit keeps its hot
// set instead of collapsing to a 0% hit rate every pass.
escapeCache.delete(escapeCache.keys().next().value as string)
}
escapeCache.set(pattern, escaped)
return escaped
}

@@ -15,10 +15,65 @@ /** A generated file: its name (with extension) and TypeScript source. */

readonly typesOnly?: boolean
/**
* Extension used on every relative re-export specifier. `'js'` (default) is
* the standard TS NodeNext form (`./x.js` resolving to a sibling `x.ts`);
* `'ts'` emits the literal on-disk path so the output runs directly under
* Node's type stripping.
*/
readonly importExt?: 'js' | 'ts'
}
// Generated files declare their public surface with these two forms, so we can
// recover the export names from the source text without parsing it.
const TYPE_EXPORT_RE = /^export type (\w+)/gm
const CONST_EXPORT_RE = /^export const (\w+)/gm
// Generated files declare their public surface with `export type <Name>` /
// `export const <Name>` at line starts, so the names can be recovered from the
// source text without parsing it.
/** Reads the identifier following `prefix` when `content` starts with it at `at`. */
const exportNameAt = (content: string, at: number, prefix: string): string | null => {
if (!content.startsWith(prefix, at)) return null
let end = at + prefix.length
while (end < content.length) {
const code = content.charCodeAt(end)
const isWord =
(code >= 97 && code <= 122) || (code >= 65 && code <= 90) || (code >= 48 && code <= 57) || code === 95
if (!isWord) break
end++
}
return end > at + prefix.length ? content.slice(at + prefix.length, end) : null
}
/** True for every JS LineTerminator code unit — the same set the old `/m` regexes anchored after. */
const isLineTerminator = (code: number): boolean => code === 10 || code === 13 || code === 8232 || code === 8233
/**
* Collects `export type` / `export const` names with a single line-start walk.
* A multiline-anchored regex scan (`/^export .../gm`) does the same job but
* showed up at several percent of total generation time in CPU profiles — the
* regex engine re-anchors at every line of every generated file on every build.
*/
const collectExportNames = (content: string, typeNames: string[], constNames: string[]): void => {
let lineStart = 0
while (lineStart < content.length) {
// charCode prefilter: almost every line starts with whitespace, a brace, or
// a keyword other than `export` — one integer compare skips the substring
// comparison for all of them (101 === 'e').
if (content.charCodeAt(lineStart) === 101 && content.startsWith('export ', lineStart)) {
const typeName = exportNameAt(content, lineStart, 'export type ')
if (typeName !== null) {
typeNames.push(typeName)
} else {
const constName = exportNameAt(content, lineStart, 'export const ')
if (constName !== null) constNames.push(constName)
}
}
// Advance past the next line break of ANY JS flavor (LF, CR, CRLF,
// U+2028, U+2029) — matching the multiline regexes this walk replaced,
// which treated all of them as line starts.
let next = lineStart
while (next < content.length && !isLineTerminator(content.charCodeAt(next))) next++
if (next >= content.length) break
// \r\n counts as one break.
lineStart = content.charCodeAt(next) === 13 && content.charCodeAt(next + 1) === 10 ? next + 2 : next + 1
}
}
/**
* Builds the `index.ts` barrel that re-exports every generated module. This is

@@ -38,2 +93,3 @@ * the shared version of the near-identical barrel each generator used to build

const typesOnly = options.typesOnly ?? false
const importExt = options.importExt ?? 'js'

@@ -50,13 +106,12 @@ const sortedFiles = files

for (const match of file.content.matchAll(TYPE_EXPORT_RE)) typeNames.push(match[1] as string)
for (const match of file.content.matchAll(CONST_EXPORT_RE)) constNames.push(match[1] as string)
collectExportNames(file.content, typeNames, constNames)
if (typeNames.length === 0 && constNames.length === 0) continue
// `.js` extension so the barrel resolves under Node ESM, not only Bun.
// An explicit extension so the barrel resolves under Node ESM, not only Bun.
if (typesOnly) {
indexContent += `export type { ${typeNames.join(', ')} } from './${moduleName}.js';\n`
indexContent += `export type { ${typeNames.join(', ')} } from './${moduleName}.${importExt}';\n`
} else {
const typeExports = typeNames.map((name) => `type ${name}`)
indexContent += `export { ${[...typeExports, ...constNames].join(', ')} } from './${moduleName}.js';\n`
indexContent += `export { ${[...typeExports, ...constNames].join(', ')} } from './${moduleName}.${importExt}';\n`
}

@@ -63,0 +118,0 @@ }

@@ -1,2 +0,13 @@

/** Parses the items of an array with a parser function */
/**
* Parses the items of an array with a parser function.
*
* When every element parses to the very same reference — the parser found it
* already valid and handed it back, as the generated item sub-parsers do for
* clean values — the input array itself is returned and nothing is allocated.
* The result array is only materialized (lazily, on the first element the
* parser actually replaced) when something was coerced or stripped, so the
* common all-clean parse costs no allocation at all. Callers already share
* element references either way; sharing the container mirrors the generated
* fast paths, which return the input array by reference too.
*/
export const validateArray = (input: unknown, parser: (input: unknown) => unknown) => {

@@ -7,10 +18,17 @@ if (!Array.isArray(input)) {

// Pre-allocate the result array for better performance than push()
const len = input.length
const result = new Array(len)
let result: unknown[] | null = null
for (let i = 0; i < len; i++) {
result[i] = parser(input[i])
const parsed = parser(input[i])
if (result !== null) {
result[i] = parsed
} else if (parsed !== input[i]) {
// First replaced element: materialize the copy and backfill the prefix.
result = new Array(len)
for (let j = 0; j < i; j++) result[j] = input[j]
result[i] = parsed
}
}
return result
return result ?? input
}