@navai/voice-frontend 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -6,6 +6,7 @@ Frontend helpers to integrate OpenAI Realtime voice without creating custom base
6
6
 
7
7
  - Route resolution helpers.
8
8
  - Optional dynamic frontend function loader.
9
+ - `useWebVoiceAgent(...)` to bootstrap voice session from React with backend + frontend tools.
9
10
  - `buildNavaiAgent(...)` with built-in tools:
10
11
  - `navigate_to`
11
12
  - `execute_app_function`
@@ -23,6 +24,21 @@ Frontend helpers to integrate OpenAI Realtime voice without creating custom base
23
24
  npm install @navai/voice-frontend @openai/agents zod
24
25
  ```
25
26
 
27
+ Peer dependency for hooks:
28
+
29
+ ```bash
30
+ npm install react
31
+ ```
32
+
33
+ When installed from npm, this package auto-configures missing scripts in the consumer `package.json`:
34
+
35
+ - `generate:module-loaders` -> `navai-generate-web-loaders`
36
+ - `predev`, `prebuild`, `pretypecheck`, `prelint` -> `npm run generate:module-loaders`
37
+
38
+ It only adds missing entries and never overwrites existing scripts.
39
+ To disable auto-setup, set `NAVAI_SKIP_AUTO_SETUP=1` (or `NAVAI_SKIP_FRONTEND_AUTO_SETUP=1`) during install.
40
+ To run setup manually later, use `npx navai-setup-voice-frontend`.
41
+
26
42
  ## Expected app inputs
27
43
 
28
44
  1. Route data in `src/ai/routes.ts` (or any array compatible with `NavaiRoute[]`).
@@ -49,6 +65,21 @@ const { agent } = await buildNavaiAgent({
49
65
 
50
66
  Then use `agent` with `RealtimeSession` from `@openai/agents/realtime`.
51
67
 
68
+ ## React hook usage
69
+
70
+ ```ts
71
+ import { useWebVoiceAgent } from "@navai/voice-frontend";
72
+ import { NAVAI_WEB_MODULE_LOADERS } from "./ai/generated-module-loaders";
73
+ import { NAVAI_ROUTE_ITEMS } from "./ai/routes";
74
+
75
+ const agent = useWebVoiceAgent({
76
+ navigate: (path) => routerNavigate(path),
77
+ moduleLoaders: NAVAI_WEB_MODULE_LOADERS,
78
+ defaultRoutes: NAVAI_ROUTE_ITEMS,
79
+ env: import.meta.env as Record<string, string | undefined>
80
+ });
81
+ ```
82
+
52
83
  ## Optional dynamic frontend functions (bundler adapter)
53
84
 
54
85
  If your bundler can provide module loaders, you can add local frontend functions too.
@@ -56,9 +87,9 @@ If your bundler can provide module loaders, you can add local frontend functions
56
87
  ```ts
57
88
  import { resolveNavaiFrontendRuntimeConfig } from "@navai/voice-frontend";
58
89
 
59
- // Vite adapter example:
90
+ // Vite adapter example (folder-scoped):
60
91
  const runtime = await resolveNavaiFrontendRuntimeConfig({
61
- moduleLoaders: import.meta.glob(["/src/**/*.{ts,js}", "!/src/ai/routes.ts"]),
92
+ moduleLoaders: import.meta.glob(["/src/ai/functions-modules/**/*.{ts,js}"]),
62
93
  defaultRoutes: NAVAI_ROUTE_ITEMS
63
94
  });
64
95
  ```
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+
6
+ const PACKAGE_NAME = "@navai/voice-frontend";
7
+ const SKIP_ENV_KEYS = ["NAVAI_SKIP_AUTO_SETUP", "NAVAI_SKIP_FRONTEND_AUTO_SETUP"];
8
+ const SCRIPT_ENTRIES = [
9
+ ["generate:module-loaders", "navai-generate-web-loaders"],
10
+ ["predev", "npm run generate:module-loaders"],
11
+ ["prebuild", "npm run generate:module-loaders"],
12
+ ["pretypecheck", "npm run generate:module-loaders"],
13
+ ["prelint", "npm run generate:module-loaders"]
14
+ ];
15
+
16
+ function shouldSkipSetup() {
17
+ for (const envKey of SKIP_ENV_KEYS) {
18
+ const value = process.env[envKey];
19
+ if (!value) {
20
+ continue;
21
+ }
22
+
23
+ const normalized = value.trim().toLowerCase();
24
+ if (normalized === "1" || normalized === "true" || normalized === "yes") {
25
+ return true;
26
+ }
27
+ }
28
+
29
+ return false;
30
+ }
31
+
32
+ function getConsumerRoot() {
33
+ const initCwd = process.env.INIT_CWD;
34
+ if (typeof initCwd === "string" && initCwd.trim().length > 0) {
35
+ return path.resolve(initCwd);
36
+ }
37
+
38
+ return process.cwd();
39
+ }
40
+
41
+ function hasPackageDependency(packageJson, packageName) {
42
+ const dependencyKeys = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
43
+
44
+ for (const key of dependencyKeys) {
45
+ const block = packageJson[key];
46
+ if (!block || typeof block !== "object") {
47
+ continue;
48
+ }
49
+
50
+ if (typeof block[packageName] === "string") {
51
+ return true;
52
+ }
53
+ }
54
+
55
+ return false;
56
+ }
57
+
58
+ async function readJson(filePath) {
59
+ try {
60
+ const raw = await readFile(filePath, "utf8");
61
+ return JSON.parse(raw);
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ async function main() {
68
+ if (shouldSkipSetup()) {
69
+ return;
70
+ }
71
+
72
+ const consumerRoot = getConsumerRoot();
73
+ const packageJsonPath = path.resolve(consumerRoot, "package.json");
74
+ const packageJson = await readJson(packageJsonPath);
75
+ if (!packageJson || typeof packageJson !== "object") {
76
+ return;
77
+ }
78
+
79
+ if (!hasPackageDependency(packageJson, PACKAGE_NAME)) {
80
+ return;
81
+ }
82
+
83
+ const scripts = packageJson.scripts && typeof packageJson.scripts === "object" ? packageJson.scripts : {};
84
+ const nextScripts = { ...scripts };
85
+ const added = [];
86
+ const kept = [];
87
+
88
+ for (const [scriptName, scriptValue] of SCRIPT_ENTRIES) {
89
+ if (typeof nextScripts[scriptName] === "string") {
90
+ if (nextScripts[scriptName] !== scriptValue) {
91
+ kept.push(scriptName);
92
+ }
93
+ continue;
94
+ }
95
+
96
+ nextScripts[scriptName] = scriptValue;
97
+ added.push(scriptName);
98
+ }
99
+
100
+ if (added.length === 0) {
101
+ return;
102
+ }
103
+
104
+ const nextPackageJson = {
105
+ ...packageJson,
106
+ scripts: nextScripts
107
+ };
108
+ await writeFile(packageJsonPath, `${JSON.stringify(nextPackageJson, null, 2)}\n`, "utf8");
109
+
110
+ console.log(`[navai] ${PACKAGE_NAME} configured scripts in ${packageJsonPath}: ${added.join(", ")}.`);
111
+ if (kept.length > 0) {
112
+ console.log(`[navai] ${PACKAGE_NAME} kept existing scripts: ${kept.join(", ")}.`);
113
+ }
114
+ }
115
+
116
+ main().catch((error) => {
117
+ const message = error instanceof Error ? error.message : String(error);
118
+ console.warn(`[navai] ${PACKAGE_NAME} auto-setup skipped: ${message}`);
119
+ });
@@ -0,0 +1,483 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+
6
+ const DEFAULT_EXTENSIONS = [
7
+ ".ts",
8
+ ".tsx",
9
+ ".js",
10
+ ".jsx",
11
+ ".mts",
12
+ ".mtsx",
13
+ ".cts",
14
+ ".ctsx",
15
+ ".mjs",
16
+ ".mjsx",
17
+ ".cjs",
18
+ ".cjsx"
19
+ ];
20
+
21
+ async function main() {
22
+ const args = parseArgs(process.argv.slice(2));
23
+ if (args.help) {
24
+ printHelp();
25
+ return;
26
+ }
27
+
28
+ const projectRoot = path.resolve(process.cwd(), args.projectRoot ?? ".");
29
+ const srcRoot = path.resolve(projectRoot, args.srcRoot ?? "src");
30
+ const outputFile = path.resolve(projectRoot, args.outputFile ?? "src/ai/generated-module-loaders.ts");
31
+ const outputDir = path.dirname(outputFile);
32
+ const envFile = path.resolve(projectRoot, args.envFile ?? ".env");
33
+ const defaultFunctionsFolder = args.defaultFunctionsFolder ?? "src/ai/functions-modules";
34
+ const defaultRoutesFile = args.defaultRoutesFile ?? "src/ai/routes.ts";
35
+ const typeImport = args.typeImport ?? "@navai/voice-frontend";
36
+ const exportName = args.exportName ?? "NAVAI_WEB_MODULE_LOADERS";
37
+
38
+ const envFileValues = await readEnvFile(envFile);
39
+ const configuredFunctionsFolders =
40
+ readOptional(process.env.NAVAI_FUNCTIONS_FOLDERS) ??
41
+ readOptional(envFileValues.NAVAI_FUNCTIONS_FOLDERS);
42
+ const configuredRoutesFile =
43
+ readOptional(process.env.NAVAI_ROUTES_FILE) ?? readOptional(envFileValues.NAVAI_ROUTES_FILE);
44
+ const functionsFolders = configuredFunctionsFolders ?? defaultFunctionsFolder;
45
+ const routesFile = configuredRoutesFile ?? defaultRoutesFile;
46
+
47
+ const defaultRoutesModuleFile = await resolveModulePathFromFs(defaultRoutesFile, projectRoot);
48
+
49
+ const configuredFunctionTokens = functionsFolders
50
+ .split(",")
51
+ .map((token) => token.trim())
52
+ .filter(Boolean);
53
+ const functionTokens =
54
+ configuredFunctionTokens.length > 0 ? configuredFunctionTokens : [defaultFunctionsFolder];
55
+ const functionMatchers = functionTokens.map((token) => createPathMatcher(token));
56
+
57
+ let selectedFunctionFiles = await collectFilesForTokens({
58
+ tokens: functionTokens,
59
+ projectRoot,
60
+ srcRoot,
61
+ outputFile
62
+ });
63
+ selectedFunctionFiles = dedupeAbsolutePaths(selectedFunctionFiles).filter((absolutePath) => {
64
+ const sourcePath = sourcePathFromAbsolute(srcRoot, absolutePath);
65
+ return functionMatchers.some((matcher) => matcher(sourcePath));
66
+ });
67
+
68
+ if (selectedFunctionFiles.length === 0 && configuredFunctionTokens.length > 0) {
69
+ const fallbackMatcher = createPathMatcher(defaultFunctionsFolder);
70
+ selectedFunctionFiles = await collectFilesForTokens({
71
+ tokens: [defaultFunctionsFolder],
72
+ projectRoot,
73
+ srcRoot,
74
+ outputFile
75
+ });
76
+ selectedFunctionFiles = dedupeAbsolutePaths(selectedFunctionFiles).filter((absolutePath) =>
77
+ fallbackMatcher(sourcePathFromAbsolute(srcRoot, absolutePath))
78
+ );
79
+ console.warn(
80
+ `[navai] NAVAI_FUNCTIONS_FOLDERS did not match any module: "${functionsFolders}". Falling back to "${defaultFunctionsFolder}".`
81
+ );
82
+ }
83
+
84
+ const selectedFiles = new Set(selectedFunctionFiles);
85
+ const routesModuleFile = await resolveModulePathFromFs(routesFile, projectRoot);
86
+ if (routesModuleFile) {
87
+ const shouldIncludeConfiguredRoutes =
88
+ !defaultRoutesModuleFile ||
89
+ path.resolve(routesModuleFile) !== path.resolve(defaultRoutesModuleFile);
90
+ if (shouldIncludeConfiguredRoutes) {
91
+ selectedFiles.add(routesModuleFile);
92
+ }
93
+ } else {
94
+ console.warn(`[navai] Route module "${routesFile}" was not found in src.`);
95
+ }
96
+
97
+ const orderedFiles = [...selectedFiles].sort((left, right) => left.localeCompare(right));
98
+ const entries = orderedFiles.map((absolutePath) => {
99
+ const sourcePath = normalizePath(path.relative(projectRoot, absolutePath));
100
+ const importPath = toRelativeModulePath(outputDir, absolutePath);
101
+ return ` "${sourcePath}": async () => await import("${importPath}")`;
102
+ });
103
+
104
+ const output = [
105
+ "// Auto-generated by @navai/voice-frontend (navai-generate-web-loaders)",
106
+ `import type { NavaiFunctionModuleLoaders } from "${typeImport}";`,
107
+ "",
108
+ `export const ${exportName}: NavaiFunctionModuleLoaders = {`,
109
+ entries.join(",\n"),
110
+ "};",
111
+ ""
112
+ ].join("\n");
113
+
114
+ await mkdir(outputDir, { recursive: true });
115
+ await writeFile(outputFile, output, "utf8");
116
+
117
+ console.log(
118
+ `[navai] Generated ${entries.length} web module loader(s) at ${normalizePath(
119
+ path.relative(projectRoot, outputFile)
120
+ )}.`
121
+ );
122
+ }
123
+
124
+ function parseArgs(argv) {
125
+ const args = {};
126
+
127
+ for (let index = 0; index < argv.length; index += 1) {
128
+ const token = argv[index];
129
+ if (token === "--help" || token === "-h") {
130
+ args.help = true;
131
+ continue;
132
+ }
133
+
134
+ if (!token.startsWith("--")) {
135
+ throw new Error(`Unknown argument: "${token}"`);
136
+ }
137
+
138
+ const equalsIndex = token.indexOf("=");
139
+ if (equalsIndex >= 0) {
140
+ const key = token.slice(2, equalsIndex);
141
+ const value = token.slice(equalsIndex + 1);
142
+ setArgValue(args, key, value);
143
+ continue;
144
+ }
145
+
146
+ const key = token.slice(2);
147
+ const next = argv[index + 1];
148
+ if (!next || next.startsWith("--")) {
149
+ throw new Error(`Missing value for argument "--${key}"`);
150
+ }
151
+
152
+ setArgValue(args, key, next);
153
+ index += 1;
154
+ }
155
+
156
+ return args;
157
+ }
158
+
159
+ function setArgValue(args, key, value) {
160
+ const map = {
161
+ "project-root": "projectRoot",
162
+ "src-root": "srcRoot",
163
+ "output-file": "outputFile",
164
+ "env-file": "envFile",
165
+ "default-functions-folder": "defaultFunctionsFolder",
166
+ "default-routes-file": "defaultRoutesFile",
167
+ "type-import": "typeImport",
168
+ "export-name": "exportName"
169
+ };
170
+
171
+ const mapped = map[key];
172
+ if (!mapped) {
173
+ throw new Error(`Unknown argument: "--${key}"`);
174
+ }
175
+
176
+ args[mapped] = value;
177
+ }
178
+
179
+ function printHelp() {
180
+ const help = [
181
+ "navai-generate-web-loaders",
182
+ "",
183
+ "Generates src/ai/generated-module-loaders.ts using NAVAI_FUNCTIONS_FOLDERS and NAVAI_ROUTES_FILE.",
184
+ "",
185
+ "Usage:",
186
+ " navai-generate-web-loaders [--project-root <path>] [--env-file <path>]",
187
+ "",
188
+ "Options:",
189
+ " --project-root <path> Project root (default: current directory)",
190
+ " --src-root <path> Source root (default: src)",
191
+ " --output-file <path> Output file (default: src/ai/generated-module-loaders.ts)",
192
+ " --env-file <path> Env file relative to project root (default: .env)",
193
+ " --default-functions-folder <path> Fallback functions folder (default: src/ai/functions-modules)",
194
+ " --default-routes-file <path> Fallback routes file (default: src/ai/routes.ts)",
195
+ " --type-import <module> Type import path (default: @navai/voice-frontend)",
196
+ " --export-name <identifier> Export constant name (default: NAVAI_WEB_MODULE_LOADERS)",
197
+ " --help Show this help"
198
+ ].join("\n");
199
+
200
+ console.log(help);
201
+ }
202
+
203
+ async function readEnvFile(envPath) {
204
+ try {
205
+ const contents = await readFile(envPath, "utf8");
206
+ return parseEnv(contents);
207
+ } catch {
208
+ return {};
209
+ }
210
+ }
211
+
212
+ function parseEnv(contents) {
213
+ const result = {};
214
+ const lines = contents.split(/\r?\n/);
215
+ for (const rawLine of lines) {
216
+ const line = rawLine.trim();
217
+ if (!line || line.startsWith("#")) {
218
+ continue;
219
+ }
220
+
221
+ const eqIndex = line.indexOf("=");
222
+ if (eqIndex <= 0) {
223
+ continue;
224
+ }
225
+
226
+ const key = line.slice(0, eqIndex).trim();
227
+ let value = line.slice(eqIndex + 1).trim();
228
+ if (
229
+ (value.startsWith("\"") && value.endsWith("\"")) ||
230
+ (value.startsWith("'") && value.endsWith("'"))
231
+ ) {
232
+ value = value.slice(1, -1);
233
+ }
234
+
235
+ result[key] = value;
236
+ }
237
+
238
+ return result;
239
+ }
240
+
241
+ function readOptional(input) {
242
+ if (typeof input !== "string") {
243
+ return undefined;
244
+ }
245
+
246
+ const trimmed = input.trim();
247
+ return trimmed.length > 0 ? trimmed : undefined;
248
+ }
249
+
250
+ function normalizePath(filePath) {
251
+ return filePath.replace(/\\/g, "/");
252
+ }
253
+
254
+ function normalizeSourcePath(input) {
255
+ return normalizePath(input)
256
+ .trim()
257
+ .replace(/^\/+/, "")
258
+ .replace(/^(\.\/)+/, "")
259
+ .replace(/^(\.\.\/)+/, "");
260
+ }
261
+
262
+ function ensureSrcPrefix(input) {
263
+ const normalized = normalizeSourcePath(input);
264
+ if (!normalized) {
265
+ return "";
266
+ }
267
+
268
+ return normalized.startsWith("src/") ? normalized : `src/${normalized}`;
269
+ }
270
+
271
+ function toRelativeModulePath(outputDir, absoluteFilePath) {
272
+ const relative = normalizePath(path.relative(outputDir, absoluteFilePath));
273
+ const withPrefix = relative.startsWith(".") ? relative : `./${relative}`;
274
+ return withPrefix.replace(/\.[cm]?[jt]sx?$/i, "");
275
+ }
276
+
277
+ function shouldIncludeFile(filename) {
278
+ if (!/\.[cm]?[jt]sx?$/i.test(filename)) {
279
+ return false;
280
+ }
281
+
282
+ return !/\.d\.ts$/i.test(filename);
283
+ }
284
+
285
+ function dedupeAbsolutePaths(absolutePaths) {
286
+ const deduped = new Map();
287
+ for (const absolutePath of absolutePaths) {
288
+ const resolved = path.resolve(absolutePath);
289
+ if (!deduped.has(resolved)) {
290
+ deduped.set(resolved, resolved);
291
+ }
292
+ }
293
+
294
+ return [...deduped.values()].sort((left, right) => left.localeCompare(right));
295
+ }
296
+
297
+ async function collectFilesForTokens(input) {
298
+ const files = [];
299
+ for (const token of input.tokens) {
300
+ files.push(...(await collectFilesForToken({ ...input, token })));
301
+ }
302
+
303
+ return files;
304
+ }
305
+
306
+ async function collectFilesForToken(input) {
307
+ const normalizedToken = ensureSrcPrefix(input.token);
308
+ if (!normalizedToken) {
309
+ return [];
310
+ }
311
+
312
+ const matcher = createPathMatcher(normalizedToken);
313
+
314
+ if (normalizedToken.endsWith("/...")) {
315
+ const base = normalizedToken.slice(0, -4).replace(/\/+$/, "");
316
+ const baseDirectory = path.resolve(input.projectRoot, base);
317
+ return await collectFilesFromDirectoryWithMatcher(baseDirectory, input, matcher);
318
+ }
319
+
320
+ if (normalizedToken.includes("*")) {
321
+ const wildcardBase = getWildcardSearchBase(normalizedToken);
322
+ const baseDirectory = path.resolve(input.projectRoot, wildcardBase);
323
+ return await collectFilesFromDirectoryWithMatcher(baseDirectory, input, matcher);
324
+ }
325
+
326
+ if (/\.[cm]?[jt]sx?$/i.test(normalizedToken)) {
327
+ const resolved = await resolveModulePathFromFs(normalizedToken, input.projectRoot);
328
+ if (!resolved || path.resolve(resolved) === path.resolve(input.outputFile)) {
329
+ return [];
330
+ }
331
+
332
+ return [resolved];
333
+ }
334
+
335
+ const possibleDirectory = path.resolve(input.projectRoot, normalizedToken);
336
+ const directoryStats = await safeStat(possibleDirectory);
337
+ if (directoryStats?.isDirectory()) {
338
+ return await walkDirectory(possibleDirectory, input.outputFile);
339
+ }
340
+
341
+ const resolved = await resolveModulePathFromFs(normalizedToken, input.projectRoot);
342
+ if (!resolved || path.resolve(resolved) === path.resolve(input.outputFile)) {
343
+ return [];
344
+ }
345
+
346
+ return [resolved];
347
+ }
348
+
349
+ async function collectFilesFromDirectoryWithMatcher(directoryPath, input, matcher) {
350
+ const files = await walkDirectoryIfExists(directoryPath, input.outputFile);
351
+ return files.filter((absolutePath) => matcher(sourcePathFromAbsolute(input.srcRoot, absolutePath)));
352
+ }
353
+
354
+ function getWildcardSearchBase(pattern) {
355
+ const wildcardIndex = pattern.indexOf("*");
356
+ if (wildcardIndex < 0) {
357
+ return pattern;
358
+ }
359
+
360
+ const prefix = pattern.slice(0, wildcardIndex);
361
+ if (!prefix) {
362
+ return "src";
363
+ }
364
+
365
+ const directoryPrefix = prefix.endsWith("/")
366
+ ? prefix
367
+ : prefix.slice(0, prefix.lastIndexOf("/") + 1);
368
+ const trimmed = directoryPrefix.replace(/\/+$/, "");
369
+ return trimmed || "src";
370
+ }
371
+
372
+ async function walkDirectoryIfExists(directoryPath, outputFile) {
373
+ const directoryStats = await safeStat(directoryPath);
374
+ if (!directoryStats?.isDirectory()) {
375
+ return [];
376
+ }
377
+
378
+ return await walkDirectory(directoryPath, outputFile);
379
+ }
380
+
381
+ async function safeStat(filePath) {
382
+ try {
383
+ return await stat(filePath);
384
+ } catch {
385
+ return null;
386
+ }
387
+ }
388
+
389
+ async function walkDirectory(directoryPath, outputFile) {
390
+ const entries = await readdir(directoryPath, { withFileTypes: true });
391
+ const files = [];
392
+
393
+ for (const entry of entries) {
394
+ const absoluteEntryPath = path.join(directoryPath, entry.name);
395
+ if (entry.isDirectory()) {
396
+ files.push(...(await walkDirectory(absoluteEntryPath, outputFile)));
397
+ continue;
398
+ }
399
+
400
+ if (!entry.isFile() || !shouldIncludeFile(entry.name)) {
401
+ continue;
402
+ }
403
+
404
+ if (path.resolve(absoluteEntryPath) === path.resolve(outputFile)) {
405
+ continue;
406
+ }
407
+
408
+ files.push(path.resolve(absoluteEntryPath));
409
+ }
410
+
411
+ return files;
412
+ }
413
+
414
+ function sourcePathFromAbsolute(srcRoot, absoluteFilePath) {
415
+ return ensureSrcPrefix(path.relative(srcRoot, absoluteFilePath));
416
+ }
417
+
418
+ function globToRegExp(pattern) {
419
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
420
+ const wildcardSafe = escaped.replace(/\*\*/g, "___DOUBLE_STAR___");
421
+ const single = wildcardSafe.replace(/\*/g, "[^/]*");
422
+ const output = single.replace(/___DOUBLE_STAR___/g, ".*");
423
+ return new RegExp(`^${output}$`);
424
+ }
425
+
426
+ function createPathMatcher(input) {
427
+ const normalized = ensureSrcPrefix(input);
428
+ if (!normalized) {
429
+ return () => false;
430
+ }
431
+
432
+ if (normalized.endsWith("/...")) {
433
+ const base = normalized.slice(0, -4).replace(/\/+$/, "");
434
+ return (candidatePath) => candidatePath.startsWith(`${base}/`);
435
+ }
436
+
437
+ if (normalized.includes("*")) {
438
+ const regexp = globToRegExp(normalized);
439
+ return (candidatePath) => regexp.test(candidatePath);
440
+ }
441
+
442
+ if (/\.[cm]?[jt]sx?$/i.test(normalized)) {
443
+ return (candidatePath) => candidatePath === normalized;
444
+ }
445
+
446
+ const base = normalized.replace(/\/+$/, "");
447
+ return (candidatePath) => candidatePath === base || candidatePath.startsWith(`${base}/`);
448
+ }
449
+
450
+ function buildModuleCandidates(inputPath) {
451
+ const normalized = ensureSrcPrefix(inputPath);
452
+ if (!normalized) {
453
+ return [];
454
+ }
455
+
456
+ if (/\.[cm]?[jt]sx?$/.test(normalized)) {
457
+ return [normalized];
458
+ }
459
+
460
+ return [
461
+ ...DEFAULT_EXTENSIONS.map((extension) => `${normalized}${extension}`),
462
+ ...DEFAULT_EXTENSIONS.map((extension) => `${normalized}/index${extension}`)
463
+ ];
464
+ }
465
+
466
+ async function resolveModulePathFromFs(inputPath, projectRoot) {
467
+ const candidates = buildModuleCandidates(inputPath);
468
+ for (const candidate of candidates) {
469
+ const absoluteCandidate = path.resolve(projectRoot, candidate);
470
+ const candidateStats = await safeStat(absoluteCandidate);
471
+ if (candidateStats?.isFile() && shouldIncludeFile(path.basename(absoluteCandidate))) {
472
+ return absoluteCandidate;
473
+ }
474
+ }
475
+
476
+ return null;
477
+ }
478
+
479
+ main().catch((error) => {
480
+ const message = error instanceof Error ? error.message : String(error);
481
+ console.error(`[navai] Failed to generate web module loaders: ${message}`);
482
+ process.exitCode = 1;
483
+ });