Skip to content

Commit 1c988e3

Browse files
Yerazeclaude
andcommitted
fix(build): house the DSL matcher server-side — safeRegex's native re2 broke the Vite client build
src/utils/autoResponderUtils.ts is imported by frontend TriggerItem.tsx; adding homoglyph/safeRegex imports there pulled the native re2 binary into the client bundle graph (UNLOADABLE_DEPENDENCY). The matcher now lives in src/server/utils/autoResponderMatcher.ts with a comment explaining the constraint; autoResponderUtils.ts restored to its import-free pre-PR state. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n
1 parent 57ea6f6 commit 1c988e3

4 files changed

Lines changed: 163 additions & 154 deletions

File tree

src/server/meshtasticManager.autoresponder-regex.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, it, expect } from 'vitest';
22
import { applyHomoglyphOptimization } from '../utils/homoglyph.js';
3-
import { matchAutoResponderPattern } from '../utils/autoResponderUtils.js';
3+
import { matchAutoResponderPattern } from './utils/autoResponderMatcher.js';
44

55
/**
66
* Auto Responder Regex Parameter Matching Tests

src/server/meshtasticManager.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ import { autoDeleteByDistanceService } from './services/autoDeleteByDistanceServ
3535
import { MessageQueueService } from './messageQueueService.js';
3636
import { resolveAutoWelcomeDelaySeconds } from './autoWelcomeDelay.js';
3737
import { resolveAutoAckPreSendDelaySeconds } from './autoAckDelay.js';
38-
import { normalizeTriggerPatterns, normalizeTriggerChannels, matchAutoResponderPattern } from '../utils/autoResponderUtils.js';
38+
import { normalizeTriggerPatterns, normalizeTriggerChannels } from '../utils/autoResponderUtils.js';
39+
import { matchAutoResponderPattern } from './utils/autoResponderMatcher.js';
3940
import { isWithinTimeWindow } from './utils/timeWindow.js';
4041
import { compileUserRegex } from '../utils/safeRegex.js';
4142
import { shouldGateAutomations, averageStrongestNeighborUtilization, DEFAULT_AIRTIME_CUTOFF_THRESHOLD, DEFAULT_AIRTIME_CUTOFF_SOURCE, NEIGHBOR_UTIL_SAMPLE_COUNT, type AirtimeCutoffSource, type NeighborUtilContributor } from './utils/airtimeCutoff.js';
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/**
2+
* Server-only: the Meshtastic auto-responder placeholder-DSL matcher.
3+
*
4+
* Lives under src/server/utils (NOT src/utils) because it depends on
5+
* safeRegex.js -> the native re2 module; src/utils/autoResponderUtils.ts is
6+
* imported by frontend components (TriggerItem.tsx) and must stay free of
7+
* Node-native dependencies or the Vite client build fails (UNLOADABLE_DEPENDENCY).
8+
*/
9+
import { applyHomoglyphOptimization } from '../../utils/homoglyph.js';
10+
import { compileUserRegex } from '../../utils/safeRegex.js';
11+
12+
export interface AutoResponderMatch {
13+
matched: boolean;
14+
/** param name -> extracted value (from original text when possible) */
15+
params: Record<string, string>;
16+
}
17+
18+
/**
19+
* Match one incoming message against one Meshtastic auto-responder trigger PATTERN
20+
* (a single comma-split pattern, e.g. "weather {city}" or "zip {z:\\d{5}}").
21+
* Homoglyph-normalizes both sides, parses the {name}/{name:regex} placeholder DSL,
22+
* builds an anchored capture regex via compileUserRegex, and extracts params from the
23+
* ORIGINAL text where possible (Unicode preservation).
24+
*/
25+
export function matchAutoResponderPattern(patternStr: string, messageText: string): AutoResponderMatch {
26+
// Normalize trigger pattern through homoglyph mapping to match normalized message text
27+
const normalizedPatternStr = applyHomoglyphOptimization(patternStr);
28+
// Normalize message text through homoglyph mapping (Issue #2136)
29+
const normalizedText = applyHomoglyphOptimization(messageText);
30+
31+
// Extract parameters with optional regex patterns from trigger pattern
32+
interface ParamSpec {
33+
name: string;
34+
pattern?: string;
35+
}
36+
const params: ParamSpec[] = [];
37+
let i = 0;
38+
39+
while (i < normalizedPatternStr.length) {
40+
if (normalizedPatternStr[i] === '{') {
41+
const startPos = i + 1;
42+
let depth = 1;
43+
let colonPos = -1;
44+
let endPos = -1;
45+
46+
// Find the matching closing brace, accounting for nested braces in regex patterns
47+
for (let j = startPos; j < normalizedPatternStr.length && depth > 0; j++) {
48+
if (normalizedPatternStr[j] === '{') {
49+
depth++;
50+
} else if (normalizedPatternStr[j] === '}') {
51+
depth--;
52+
if (depth === 0) {
53+
endPos = j;
54+
}
55+
} else if (normalizedPatternStr[j] === ':' && depth === 1 && colonPos === -1) {
56+
colonPos = j;
57+
}
58+
}
59+
60+
if (endPos !== -1) {
61+
const paramName = colonPos !== -1
62+
? normalizedPatternStr.substring(startPos, colonPos)
63+
: normalizedPatternStr.substring(startPos, endPos);
64+
const paramPattern = colonPos !== -1
65+
? normalizedPatternStr.substring(colonPos + 1, endPos)
66+
: undefined;
67+
68+
if (!params.find(p => p.name === paramName)) {
69+
params.push({ name: paramName, pattern: paramPattern });
70+
}
71+
72+
i = endPos + 1;
73+
} else {
74+
i++;
75+
}
76+
} else {
77+
i++;
78+
}
79+
}
80+
81+
// Build regex pattern from trigger by processing it character by character
82+
let pattern = '';
83+
const replacements: Array<{ start: number; end: number; replacement: string }> = [];
84+
i = 0;
85+
86+
while (i < normalizedPatternStr.length) {
87+
if (normalizedPatternStr[i] === '{') {
88+
const startPos = i;
89+
let depth = 1;
90+
let endPos = -1;
91+
92+
// Find the matching closing brace
93+
for (let j = i + 1; j < normalizedPatternStr.length && depth > 0; j++) {
94+
if (normalizedPatternStr[j] === '{') {
95+
depth++;
96+
} else if (normalizedPatternStr[j] === '}') {
97+
depth--;
98+
if (depth === 0) {
99+
endPos = j;
100+
}
101+
}
102+
}
103+
104+
if (endPos !== -1) {
105+
const paramIndex = replacements.length;
106+
if (paramIndex < params.length) {
107+
const paramRegex = params[paramIndex].pattern || '[^\\s]+';
108+
replacements.push({
109+
start: startPos,
110+
end: endPos + 1,
111+
replacement: `(${paramRegex})`
112+
});
113+
}
114+
i = endPos + 1;
115+
} else {
116+
i++;
117+
}
118+
} else {
119+
i++;
120+
}
121+
}
122+
123+
// Build the final pattern by replacing placeholders
124+
for (let i = 0; i < normalizedPatternStr.length; i++) {
125+
const replacement = replacements.find(r => r.start === i);
126+
if (replacement) {
127+
pattern += replacement.replacement;
128+
i = replacement.end - 1; // -1 because loop will increment
129+
} else {
130+
// Escape special regex characters in literal parts
131+
const char = normalizedPatternStr[i];
132+
if (/[.*+?^${}()|[\]\\]/.test(char)) {
133+
pattern += '\\' + char;
134+
} else {
135+
pattern += char;
136+
}
137+
}
138+
}
139+
140+
const triggerRegex = compileUserRegex(`^${pattern}$`, 'i');
141+
const triggerMatch = normalizedText.match(triggerRegex);
142+
143+
if (triggerMatch) {
144+
// Extract parameters from original text when possible to preserve full
145+
// Unicode characters. Homoglyph normalization can mangle Cyrillic words
146+
// (e.g., "Барнаул" → "Бapнayл") which breaks geocoding APIs.
147+
// The regex usually matches original text too since param patterns like
148+
// [^\s]+ accept any non-whitespace character.
149+
const originalMatch = messageText.match(triggerRegex);
150+
151+
const extractedParams: Record<string, string> = {};
152+
params.forEach((param, index) => {
153+
extractedParams[param.name] = originalMatch?.[index + 1] ?? triggerMatch[index + 1];
154+
});
155+
return { matched: true, params: extractedParams };
156+
}
157+
158+
return { matched: false, params: {} };
159+
}
160+

src/utils/autoResponderUtils.ts

Lines changed: 0 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -2,158 +2,6 @@
22
* Utility functions for auto-responder trigger processing
33
*/
44

5-
import { applyHomoglyphOptimization } from './homoglyph.js';
6-
import { compileUserRegex } from './safeRegex.js';
7-
8-
export interface AutoResponderMatch {
9-
matched: boolean;
10-
/** param name -> extracted value (from original text when possible) */
11-
params: Record<string, string>;
12-
}
13-
14-
/**
15-
* Match one incoming message against one Meshtastic auto-responder trigger PATTERN
16-
* (a single comma-split pattern, e.g. "weather {city}" or "zip {z:\\d{5}}").
17-
* Homoglyph-normalizes both sides, parses the {name}/{name:regex} placeholder DSL,
18-
* builds an anchored capture regex via compileUserRegex, and extracts params from the
19-
* ORIGINAL text where possible (Unicode preservation).
20-
*/
21-
export function matchAutoResponderPattern(patternStr: string, messageText: string): AutoResponderMatch {
22-
// Normalize trigger pattern through homoglyph mapping to match normalized message text
23-
const normalizedPatternStr = applyHomoglyphOptimization(patternStr);
24-
// Normalize message text through homoglyph mapping (Issue #2136)
25-
const normalizedText = applyHomoglyphOptimization(messageText);
26-
27-
// Extract parameters with optional regex patterns from trigger pattern
28-
interface ParamSpec {
29-
name: string;
30-
pattern?: string;
31-
}
32-
const params: ParamSpec[] = [];
33-
let i = 0;
34-
35-
while (i < normalizedPatternStr.length) {
36-
if (normalizedPatternStr[i] === '{') {
37-
const startPos = i + 1;
38-
let depth = 1;
39-
let colonPos = -1;
40-
let endPos = -1;
41-
42-
// Find the matching closing brace, accounting for nested braces in regex patterns
43-
for (let j = startPos; j < normalizedPatternStr.length && depth > 0; j++) {
44-
if (normalizedPatternStr[j] === '{') {
45-
depth++;
46-
} else if (normalizedPatternStr[j] === '}') {
47-
depth--;
48-
if (depth === 0) {
49-
endPos = j;
50-
}
51-
} else if (normalizedPatternStr[j] === ':' && depth === 1 && colonPos === -1) {
52-
colonPos = j;
53-
}
54-
}
55-
56-
if (endPos !== -1) {
57-
const paramName = colonPos !== -1
58-
? normalizedPatternStr.substring(startPos, colonPos)
59-
: normalizedPatternStr.substring(startPos, endPos);
60-
const paramPattern = colonPos !== -1
61-
? normalizedPatternStr.substring(colonPos + 1, endPos)
62-
: undefined;
63-
64-
if (!params.find(p => p.name === paramName)) {
65-
params.push({ name: paramName, pattern: paramPattern });
66-
}
67-
68-
i = endPos + 1;
69-
} else {
70-
i++;
71-
}
72-
} else {
73-
i++;
74-
}
75-
}
76-
77-
// Build regex pattern from trigger by processing it character by character
78-
let pattern = '';
79-
const replacements: Array<{ start: number; end: number; replacement: string }> = [];
80-
i = 0;
81-
82-
while (i < normalizedPatternStr.length) {
83-
if (normalizedPatternStr[i] === '{') {
84-
const startPos = i;
85-
let depth = 1;
86-
let endPos = -1;
87-
88-
// Find the matching closing brace
89-
for (let j = i + 1; j < normalizedPatternStr.length && depth > 0; j++) {
90-
if (normalizedPatternStr[j] === '{') {
91-
depth++;
92-
} else if (normalizedPatternStr[j] === '}') {
93-
depth--;
94-
if (depth === 0) {
95-
endPos = j;
96-
}
97-
}
98-
}
99-
100-
if (endPos !== -1) {
101-
const paramIndex = replacements.length;
102-
if (paramIndex < params.length) {
103-
const paramRegex = params[paramIndex].pattern || '[^\\s]+';
104-
replacements.push({
105-
start: startPos,
106-
end: endPos + 1,
107-
replacement: `(${paramRegex})`
108-
});
109-
}
110-
i = endPos + 1;
111-
} else {
112-
i++;
113-
}
114-
} else {
115-
i++;
116-
}
117-
}
118-
119-
// Build the final pattern by replacing placeholders
120-
for (let i = 0; i < normalizedPatternStr.length; i++) {
121-
const replacement = replacements.find(r => r.start === i);
122-
if (replacement) {
123-
pattern += replacement.replacement;
124-
i = replacement.end - 1; // -1 because loop will increment
125-
} else {
126-
// Escape special regex characters in literal parts
127-
const char = normalizedPatternStr[i];
128-
if (/[.*+?^${}()|[\]\\]/.test(char)) {
129-
pattern += '\\' + char;
130-
} else {
131-
pattern += char;
132-
}
133-
}
134-
}
135-
136-
const triggerRegex = compileUserRegex(`^${pattern}$`, 'i');
137-
const triggerMatch = normalizedText.match(triggerRegex);
138-
139-
if (triggerMatch) {
140-
// Extract parameters from original text when possible to preserve full
141-
// Unicode characters. Homoglyph normalization can mangle Cyrillic words
142-
// (e.g., "Барнаул" → "Бapнayл") which breaks geocoding APIs.
143-
// The regex usually matches original text too since param patterns like
144-
// [^\s]+ accept any non-whitespace character.
145-
const originalMatch = messageText.match(triggerRegex);
146-
147-
const extractedParams: Record<string, string> = {};
148-
params.forEach((param, index) => {
149-
extractedParams[param.name] = originalMatch?.[index + 1] ?? triggerMatch[index + 1];
150-
});
151-
return { matched: true, params: extractedParams };
152-
}
153-
154-
return { matched: false, params: {} };
155-
}
156-
1575
/**
1586
* Splits a comma-separated trigger string into individual patterns.
1597
* Respects brace-enclosed parameters and doesn't split commas inside them.

0 commit comments

Comments
 (0)