@taserjs/plugin 0.2.0

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.
Files changed (48) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +21 -0
  3. package/dist/esbuild.d.ts +7 -0
  4. package/dist/esbuild.d.ts.map +1 -0
  5. package/dist/esbuild.js +8 -0
  6. package/dist/esbuild.js.map +1 -0
  7. package/dist/index.d.ts +32 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +337 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/next.d.ts +18 -0
  12. package/dist/next.d.ts.map +1 -0
  13. package/dist/next.js +90 -0
  14. package/dist/next.js.map +1 -0
  15. package/dist/nitro.d.ts +17 -0
  16. package/dist/nitro.d.ts.map +1 -0
  17. package/dist/nitro.js +149 -0
  18. package/dist/nitro.js.map +1 -0
  19. package/dist/rolldown.d.ts +6 -0
  20. package/dist/rolldown.d.ts.map +1 -0
  21. package/dist/rolldown.js +7 -0
  22. package/dist/rolldown.js.map +1 -0
  23. package/dist/rollup.d.ts +7 -0
  24. package/dist/rollup.d.ts.map +1 -0
  25. package/dist/rollup.js +8 -0
  26. package/dist/rollup.js.map +1 -0
  27. package/dist/rspack.d.ts +7 -0
  28. package/dist/rspack.d.ts.map +1 -0
  29. package/dist/rspack.js +8 -0
  30. package/dist/rspack.js.map +1 -0
  31. package/dist/vite.d.ts +7 -0
  32. package/dist/vite.d.ts.map +1 -0
  33. package/dist/vite.js +8 -0
  34. package/dist/vite.js.map +1 -0
  35. package/dist/webpack.d.ts +7 -0
  36. package/dist/webpack.d.ts.map +1 -0
  37. package/dist/webpack.js +8 -0
  38. package/dist/webpack.js.map +1 -0
  39. package/package.json +155 -0
  40. package/src/esbuild.ts +5 -0
  41. package/src/index.ts +538 -0
  42. package/src/next.ts +121 -0
  43. package/src/nitro.ts +189 -0
  44. package/src/rolldown.ts +5 -0
  45. package/src/rollup.ts +5 -0
  46. package/src/rspack.ts +5 -0
  47. package/src/vite.ts +20 -0
  48. package/src/webpack.ts +5 -0
package/src/index.ts ADDED
@@ -0,0 +1,538 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { isAbsolute, join, normalize, relative, resolve } from "pathe";
3
+ import {
4
+ generateManifest,
5
+ loadConfig,
6
+ resolveAppFile,
7
+ resolveImportExtension,
8
+ resolveOutputDir,
9
+ resolveRoutesDir,
10
+ resolveServerDir,
11
+ scanRoutes,
12
+ type ResolvedTaserConfig,
13
+ } from "@taserjs/cli";
14
+ import { toFetchHandler, toNodeHandler } from "srvx/node";
15
+ import { createUnplugin } from "unplugin";
16
+
17
+ export const DEFAULT_WATCH_DEBOUNCE_MS = 50;
18
+ export const DEFAULT_OUTPUT_IGNORE_PATTERN = "**/.taserjs/**";
19
+
20
+ export interface TaserPluginOptions {
21
+ server?: boolean | undefined;
22
+ serverEntry?: string | undefined;
23
+ cwd?: string | undefined;
24
+ config?: string | undefined;
25
+ standalone?: boolean | undefined;
26
+ }
27
+
28
+ export interface HostServerInfo {
29
+ type: "node" | "fetch";
30
+ path: string;
31
+ }
32
+
33
+ const NODE_SERVER_CANDIDATES = [
34
+ "server.node.ts",
35
+ "server.node.js",
36
+ "server.node.mjs",
37
+ "server.node.cjs",
38
+ ];
39
+
40
+ const FETCH_SERVER_CANDIDATES = ["server.ts", "server.js", "server.mjs", "server.cjs"];
41
+
42
+ export function detectHostServer(
43
+ serverDir: string,
44
+ cwd?: string,
45
+ explicitEntry?: string,
46
+ ): HostServerInfo | null {
47
+ if (explicitEntry) {
48
+ const resolvedPath = isAbsolute(explicitEntry)
49
+ ? explicitEntry
50
+ : resolve(cwd ?? process.cwd(), explicitEntry);
51
+ if (existsSync(resolvedPath)) {
52
+ const isNode = resolvedPath.includes(".node.") || resolvedPath.endsWith(".node");
53
+ return {
54
+ type: isNode ? "node" : "fetch",
55
+ path: resolvedPath,
56
+ };
57
+ }
58
+ }
59
+
60
+ for (const candidate of NODE_SERVER_CANDIDATES) {
61
+ const candidatePath = join(serverDir, candidate);
62
+ if (existsSync(candidatePath)) {
63
+ return { type: "node", path: candidatePath };
64
+ }
65
+ }
66
+
67
+ for (const candidate of FETCH_SERVER_CANDIDATES) {
68
+ const candidatePath = join(serverDir, candidate);
69
+ if (existsSync(candidatePath)) {
70
+ return { type: "fetch", path: candidatePath };
71
+ }
72
+ }
73
+
74
+ return null;
75
+ }
76
+
77
+ const FULLSTACK_PLUGIN_PATTERNS = [
78
+ "nitro",
79
+ "tanstack-start",
80
+ "tanstack:router",
81
+ "react-router",
82
+ "remix",
83
+ "astro",
84
+ "sveltekit",
85
+ ];
86
+
87
+ function isFullStackPluginName(name: string): boolean {
88
+ const lower = name.toLowerCase();
89
+ for (const pattern of FULLSTACK_PLUGIN_PATTERNS) {
90
+ if (lower.includes(pattern)) {
91
+ return true;
92
+ }
93
+ }
94
+ if (lower.includes("tanstack") && lower.includes("start")) {
95
+ return true;
96
+ }
97
+ return false;
98
+ }
99
+
100
+ function detectFullStack(viteConfig: any): boolean {
101
+ if (!viteConfig) return false;
102
+ if (viteConfig.nitro) return true;
103
+ const plugins = viteConfig.plugins;
104
+ if (!plugins) return false;
105
+ const flat = Array.isArray(plugins) ? plugins.flat(Infinity) : [plugins];
106
+ return flat.some((p: any) => {
107
+ const name = p?.name;
108
+ return typeof name === "string" && isFullStackPluginName(name);
109
+ });
110
+ }
111
+
112
+ function isRunnableEnvironment(environment: any): boolean {
113
+ if (!environment) return true;
114
+ try {
115
+ if (environment.constructor?.name === "FetchableDevEnvironment") {
116
+ return false;
117
+ }
118
+ if (environment.constructor?.name === "RunnableDevEnvironment") {
119
+ return true;
120
+ }
121
+ if (typeof (environment as any).dispatchFetch === "function" && !(environment as any).runner) {
122
+ return false;
123
+ }
124
+ if ((environment as any).runner) {
125
+ return true;
126
+ }
127
+ } catch (err: unknown) {
128
+ const message = err instanceof Error ? err.message : String(err);
129
+ console.warn(`[taserjs] Warning checking Vite environment runnability: ${message}`);
130
+ }
131
+ return true;
132
+ }
133
+
134
+ export function normalizeImportPath(pathStr: string): string {
135
+ let normalized = normalize(pathStr);
136
+ if (!isAbsolute(normalized) && !normalized.startsWith("./") && !normalized.startsWith("../")) {
137
+ normalized = `./${normalized}`;
138
+ }
139
+ return normalized;
140
+ }
141
+
142
+ export function isSubPath(child: string, parent: string): boolean {
143
+ const rel = relative(parent, child);
144
+ return !rel.startsWith("../") && rel !== ".." && !isAbsolute(rel);
145
+ }
146
+
147
+ export function isOutputDir(filePath: string, outputDir: string): boolean {
148
+ const normalizedFile = resolve(filePath);
149
+ const normalizedOutput = resolve(outputDir);
150
+ return (
151
+ normalizedFile === normalizedOutput ||
152
+ isSubPath(normalizedFile, normalizedOutput) ||
153
+ filePath.includes(".taserjs")
154
+ );
155
+ }
156
+
157
+ function mergeWatchIgnored(current: unknown, ...patterns: string[]): Array<string | RegExp> {
158
+ const existing: Array<string | RegExp> = Array.isArray(current)
159
+ ? [...current]
160
+ : current
161
+ ? [current as string | RegExp]
162
+ : [];
163
+
164
+ for (const pattern of patterns) {
165
+ if (!existing.includes(pattern)) {
166
+ existing.push(pattern);
167
+ }
168
+ }
169
+
170
+ return existing;
171
+ }
172
+
173
+ export function getHostServer(
174
+ config: ResolvedTaserConfig,
175
+ cwd: string,
176
+ options?: TaserPluginOptions,
177
+ ): HostServerInfo | null {
178
+ const serverDir = resolveServerDir(config, cwd);
179
+ return detectHostServer(serverDir, cwd, options?.serverEntry);
180
+ }
181
+
182
+ export function resolveHostFetchHandler(
183
+ hostMod: unknown,
184
+ type: "node" | "fetch",
185
+ ): ((fetchReq: Request) => Promise<Response> | Response) | null {
186
+ const rawHost = (hostMod as Record<string, any>)?.default ?? hostMod;
187
+ if (type === "node") {
188
+ const nodeHandler = typeof rawHost === "function" ? rawHost : (rawHost?.routing ?? rawHost);
189
+ return typeof nodeHandler === "function" ? toFetchHandler(nodeHandler) : null;
190
+ }
191
+ if (typeof (rawHost as any)?.fetch === "function") {
192
+ return (fetchReq: Request) => (rawHost as any).fetch(fetchReq);
193
+ }
194
+ return typeof rawHost === "function" ? (rawHost as any) : null;
195
+ }
196
+
197
+ export function mountHostFallback(app: any, hostMod: unknown, type: "node" | "fetch"): boolean {
198
+ if (!app || typeof app.all !== "function" || app._hasHostFallback) {
199
+ return false;
200
+ }
201
+ const hostFetch = resolveHostFetchHandler(hostMod, type);
202
+ if (hostFetch) {
203
+ app.all("*", (c: any) => hostFetch(c.req.raw));
204
+ app._hasHostFallback = true;
205
+ return true;
206
+ }
207
+ return false;
208
+ }
209
+
210
+ export function buildHostFallbackCode(
211
+ hostImportPath: string,
212
+ type: "node" | "fetch",
213
+ appVarName: string = "app",
214
+ ): { imports: string; setup: string } {
215
+ const isNode = type === "node";
216
+ const imports = `${isNode ? 'import { toFetchHandler } from "srvx/node";\n' : ""}import hostServerEntry from "${hostImportPath}";`;
217
+
218
+ const setup = isNode
219
+ ? `const rawHost = hostServerEntry?.default ?? hostServerEntry;
220
+ const nodeHandler = typeof rawHost === "function" ? rawHost : (rawHost?.routing ?? rawHost);
221
+ const hostFetch = typeof nodeHandler === "function" ? toFetchHandler(nodeHandler) : null;
222
+ if (hostFetch) ${appVarName}.all("*", (c) => hostFetch(c.req.raw));`
223
+ : `const rawHost = hostServerEntry?.default ?? hostServerEntry;
224
+ const hostFetch = typeof rawHost?.fetch === "function" ? (r) => rawHost.fetch(r) : typeof rawHost === "function" ? rawHost : null;
225
+ if (hostFetch) ${appVarName}.all("*", (c) => hostFetch(c.req.raw));`;
226
+
227
+ return { imports, setup };
228
+ }
229
+
230
+ export function emitServeShim(
231
+ serveShimPath: string,
232
+ routesGenImportPath: string,
233
+ hostServer?: HostServerInfo | null,
234
+ ext: string = ".js",
235
+ ): void {
236
+ const normalizedRoutesPath = normalizeImportPath(routesGenImportPath);
237
+ const outputDir = resolve(serveShimPath, "..");
238
+
239
+ let code: string;
240
+ if (hostServer) {
241
+ let hostImportPath = normalizeImportPath(relative(outputDir, hostServer.path));
242
+ if (hostImportPath.endsWith(".ts")) {
243
+ hostImportPath = hostImportPath.slice(0, -3) + ext;
244
+ } else if (hostImportPath.endsWith(".js") && ext === "") {
245
+ hostImportPath = hostImportPath.slice(0, -3);
246
+ }
247
+
248
+ const fallbackCode = buildHostFallbackCode(hostImportPath, hostServer.type, "app");
249
+ code = `// @ts-nocheck
250
+ import { FastResponse } from "srvx";
251
+ globalThis.Response = FastResponse;
252
+ import { serve } from "srvx/node";
253
+ ${fallbackCode.imports}
254
+ import { app } from "${normalizedRoutesPath}";
255
+
256
+ ${fallbackCode.setup}
257
+
258
+ export { app };
259
+ serve(app);
260
+ `;
261
+ } else {
262
+ code = `// @ts-nocheck
263
+ import { FastResponse } from "srvx";
264
+ globalThis.Response = FastResponse;
265
+ import { serve } from "srvx/node";
266
+ import { app } from "${normalizedRoutesPath}";
267
+
268
+ serve(app);
269
+ `;
270
+ }
271
+
272
+ mkdirSync(resolve(serveShimPath, ".."), { recursive: true });
273
+ writeFileSync(serveShimPath, code, "utf-8");
274
+ }
275
+
276
+ export const taserPlugin = createUnplugin((options: TaserPluginOptions | undefined = {}, meta) => {
277
+ let cwd = options.cwd ? resolve(options.cwd) : process.cwd();
278
+ let cachedConfig: ResolvedTaserConfig | null = null;
279
+ let detectedFullStack: boolean | null = null;
280
+
281
+ async function getConfig(): Promise<ResolvedTaserConfig> {
282
+ if (!cachedConfig) {
283
+ cachedConfig = await loadConfig(cwd, options.config);
284
+ }
285
+ return cachedConfig;
286
+ }
287
+
288
+ async function resolveIsServerMode(viteConfig?: any): Promise<boolean> {
289
+ if (options.server !== undefined) {
290
+ return options.server;
291
+ }
292
+ const config = await getConfig();
293
+ if (config.server !== undefined) {
294
+ return config.server;
295
+ }
296
+ if (detectedFullStack !== null) {
297
+ return !detectedFullStack;
298
+ }
299
+ if (viteConfig && detectFullStack(viteConfig)) {
300
+ detectedFullStack = true;
301
+ return false;
302
+ }
303
+ return true;
304
+ }
305
+
306
+ async function executeGeneration(isDev = false): Promise<void> {
307
+ try {
308
+ const config = await getConfig();
309
+ const routesDir = resolveRoutesDir(config, cwd);
310
+ const scanResult = scanRoutes({
311
+ routesDir,
312
+ cwd,
313
+ scaffold: isDev,
314
+ formatting: config.formatting,
315
+ });
316
+
317
+ generateManifest(scanResult, config, cwd);
318
+ } catch (err: unknown) {
319
+ if (isDev) {
320
+ const message = err instanceof Error ? err.message : String(err);
321
+ console.error(`[taserjs] Route generation warning: ${message}`);
322
+ } else {
323
+ throw err;
324
+ }
325
+ }
326
+ }
327
+
328
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null;
329
+ function triggerDebouncedGeneration(): void {
330
+ if (debounceTimer) {
331
+ clearTimeout(debounceTimer);
332
+ }
333
+ debounceTimer = setTimeout(() => {
334
+ executeGeneration(true).catch(() => {});
335
+ }, DEFAULT_WATCH_DEBOUNCE_MS);
336
+ }
337
+
338
+ let cachedDevHandler: ((req: any, res: any) => any) | null = null;
339
+ const VITE_INTERNAL_PREFIXES = ["/@", "/__vite", "/__open-in-editor", "/@fs/", "/@id/"];
340
+ const VITE_QUERY_PATTERN = /[?&](?:import|raw|url|worker)\b/;
341
+
342
+ function shouldSkipDevRequest(url: string): boolean {
343
+ for (let i = 0; i < VITE_INTERNAL_PREFIXES.length; i++) {
344
+ if (url.startsWith(VITE_INTERNAL_PREFIXES[i]!)) {
345
+ return true;
346
+ }
347
+ }
348
+ if (url.startsWith("/node_modules/")) {
349
+ return true;
350
+ }
351
+ return VITE_QUERY_PATTERN.test(url);
352
+ }
353
+
354
+ return {
355
+ name: "taserjs:plugin",
356
+
357
+ async buildStart() {
358
+ const isDev = meta.framework === "vite";
359
+ await executeGeneration(isDev);
360
+
361
+ const isServerMode = await resolveIsServerMode();
362
+ if (isServerMode) {
363
+ const config = await getConfig();
364
+ const outputDir = resolveOutputDir(config, cwd);
365
+ const hostServer = getHostServer(config, cwd, options);
366
+ const serveShimPath = join(outputDir, "serve.ts");
367
+ const ext = resolveImportExtension(config.extension);
368
+ emitServeShim(serveShimPath, `./routes.gen${ext}`, hostServer, ext);
369
+ }
370
+ },
371
+
372
+ async watchChange(id, _change) {
373
+ cachedDevHandler = null;
374
+ const config = await getConfig();
375
+ const outputDir = resolveOutputDir(config, cwd);
376
+ if (isOutputDir(id, outputDir)) {
377
+ return;
378
+ }
379
+
380
+ const routesDir = resolveRoutesDir(config, cwd);
381
+ const serverDir = resolveServerDir(config, cwd);
382
+ const appFile = resolveAppFile(config, cwd);
383
+
384
+ if (id === appFile || isSubPath(id, routesDir) || isSubPath(id, serverDir)) {
385
+ await executeGeneration(true);
386
+ }
387
+ },
388
+
389
+ vite: {
390
+ async config(config, env) {
391
+ if (!options.cwd && config.root) {
392
+ cwd = resolve(config.root);
393
+ cachedConfig = null;
394
+ }
395
+ config.server = config.server || {};
396
+ config.server.watch = config.server.watch || {};
397
+ config.server.watch.ignored = mergeWatchIgnored(
398
+ config.server.watch.ignored,
399
+ DEFAULT_OUTPUT_IGNORE_PATTERN,
400
+ );
401
+
402
+ const isFullStack = detectFullStack(config);
403
+ detectedFullStack = isFullStack;
404
+
405
+ const isServerMode = await resolveIsServerMode(config);
406
+
407
+ if (isServerMode && env?.command === "build") {
408
+ const taserConfig = await getConfig();
409
+ const outputDir = resolveOutputDir(taserConfig, cwd);
410
+ const hostServer = getHostServer(taserConfig, cwd, options);
411
+ const serveShimPath = join(outputDir, "serve.ts");
412
+ const ext = resolveImportExtension(taserConfig.extension);
413
+ emitServeShim(serveShimPath, `./routes.gen${ext}`, hostServer, ext);
414
+
415
+ return {
416
+ build: {
417
+ ssr: serveShimPath,
418
+ rollupOptions: {
419
+ external: ["srvx", "srvx/node", "@taserjs/runtime", "hono"],
420
+ output: {
421
+ entryFileNames: "serve.mjs",
422
+ },
423
+ },
424
+ },
425
+ };
426
+ }
427
+ },
428
+
429
+ configResolved(resolvedConfig) {
430
+ if (detectFullStack(resolvedConfig)) {
431
+ detectedFullStack = true;
432
+ } else if (detectedFullStack === null) {
433
+ detectedFullStack = false;
434
+ }
435
+ },
436
+
437
+ async configureServer(server) {
438
+ const handleFileChange = async (file: string) => {
439
+ cachedDevHandler = null;
440
+ const config = await getConfig();
441
+ const outputDir = resolveOutputDir(config, cwd);
442
+ if (isOutputDir(file, outputDir)) {
443
+ return;
444
+ }
445
+
446
+ const routesDir = resolveRoutesDir(config, cwd);
447
+ const serverDir = resolveServerDir(config, cwd);
448
+ const appFile = resolveAppFile(config, cwd);
449
+
450
+ if (file === appFile || isSubPath(file, routesDir) || isSubPath(file, serverDir)) {
451
+ triggerDebouncedGeneration();
452
+ }
453
+ };
454
+
455
+ server.watcher.on("add", handleFileChange);
456
+ server.watcher.on("unlink", handleFileChange);
457
+ server.watcher.on("change", handleFileChange);
458
+ server.watcher.on("unlinkDir", handleFileChange);
459
+
460
+ const isServerMode = await resolveIsServerMode(server.config);
461
+ if (!isServerMode) {
462
+ return;
463
+ }
464
+
465
+ server.middlewares.use(async (req: any, res: any, next: any) => {
466
+ const url = req.url;
467
+ if (!url || shouldSkipDevRequest(url)) {
468
+ return next();
469
+ }
470
+
471
+ const ssrEnv = (server as any).environments?.ssr;
472
+ if ((server as any).environments && ssrEnv && !isRunnableEnvironment(ssrEnv)) {
473
+ return next();
474
+ }
475
+
476
+ try {
477
+ if (!cachedDevHandler) {
478
+ const taserConfig = await getConfig();
479
+ const outputDir = resolveOutputDir(taserConfig, cwd);
480
+ const routesGenPath = join(outputDir, "routes.gen.ts");
481
+
482
+ if (!existsSync(routesGenPath)) {
483
+ await executeGeneration(true);
484
+ }
485
+
486
+ const mod = (await server.ssrLoadModule(routesGenPath)) as Record<string, any>;
487
+ const app = mod.app ?? mod.default;
488
+
489
+ if (!app || typeof app.fetch !== "function") {
490
+ return next();
491
+ }
492
+
493
+ const hostServer = getHostServer(taserConfig, cwd, options);
494
+
495
+ if (hostServer) {
496
+ const hostMod = await server.ssrLoadModule(hostServer.path);
497
+ mountHostFallback(app, hostMod, hostServer.type);
498
+ }
499
+
500
+ cachedDevHandler = toNodeHandler((fetchReq: Request) => app.fetch(fetchReq));
501
+ }
502
+
503
+ if (cachedDevHandler) {
504
+ await cachedDevHandler(req, res);
505
+ } else {
506
+ next();
507
+ }
508
+ } catch (error) {
509
+ server.ssrFixStacktrace(error as Error);
510
+ next(error);
511
+ }
512
+ });
513
+ },
514
+ },
515
+
516
+ webpack(compiler) {
517
+ compiler.options.watchOptions = compiler.options.watchOptions || {};
518
+ compiler.options.watchOptions.ignored = mergeWatchIgnored(
519
+ compiler.options.watchOptions.ignored,
520
+ DEFAULT_OUTPUT_IGNORE_PATTERN,
521
+ );
522
+ },
523
+
524
+ rspack(compiler) {
525
+ compiler.options.watchOptions = compiler.options.watchOptions || {};
526
+ compiler.options.watchOptions.ignored = mergeWatchIgnored(
527
+ compiler.options.watchOptions.ignored,
528
+ DEFAULT_OUTPUT_IGNORE_PATTERN,
529
+ );
530
+ },
531
+ };
532
+ });
533
+
534
+ export const taser = Object.assign(
535
+ (options?: TaserPluginOptions) => taserPlugin.raw(options, {} as any),
536
+ taserPlugin,
537
+ );
538
+ export default taserPlugin;
package/src/next.ts ADDED
@@ -0,0 +1,121 @@
1
+ import { existsSync } from "node:fs";
2
+ import { resolve } from "pathe";
3
+ import { generateManifest, loadConfig, resolveRoutesDir, scanRoutes } from "@taserjs/cli";
4
+ import { watch, type FSWatcher } from "chokidar";
5
+ import { taserPlugin } from "./index.js";
6
+
7
+ export interface NextTaserOptions {
8
+ cwd?: string | undefined;
9
+ config?: string | undefined;
10
+ }
11
+
12
+ let turbopackWatcher: FSWatcher | null = null;
13
+
14
+ export async function runNextTaserGeneration(
15
+ cwd: string,
16
+ options?: NextTaserOptions,
17
+ ): Promise<void> {
18
+ const config = await loadConfig(cwd, options?.config);
19
+ const routesDir = resolveRoutesDir(config, cwd);
20
+ if (existsSync(routesDir)) {
21
+ const scanResult = scanRoutes({
22
+ routesDir,
23
+ cwd,
24
+ });
25
+ generateManifest(scanResult, config, cwd);
26
+ }
27
+ }
28
+
29
+ export async function startTurbopackWatcher(
30
+ cwd: string,
31
+ options?: NextTaserOptions,
32
+ ): Promise<FSWatcher | null> {
33
+ if (turbopackWatcher) {
34
+ return turbopackWatcher;
35
+ }
36
+
37
+ const config = await loadConfig(cwd, options?.config);
38
+ const routesDir = resolveRoutesDir(config, cwd);
39
+
40
+ if (existsSync(routesDir)) {
41
+ turbopackWatcher = watch([routesDir], {
42
+ ignoreInitial: true,
43
+ usePolling: true,
44
+ interval: 100,
45
+ ignored: [/(^|[/\\])\../, /(^|[/\\])-/, /node_modules/, /\.taserjs/],
46
+ });
47
+
48
+ let timer: ReturnType<typeof setTimeout> | null = null;
49
+ turbopackWatcher.on("all", () => {
50
+ if (timer) clearTimeout(timer);
51
+ timer = setTimeout(() => {
52
+ const scanResult = scanRoutes({
53
+ routesDir,
54
+ cwd,
55
+ scaffold: true,
56
+ formatting: config.formatting,
57
+ });
58
+ generateManifest(scanResult, config, cwd);
59
+ }, 50);
60
+ });
61
+ }
62
+
63
+ return turbopackWatcher;
64
+ }
65
+
66
+ export function closeTurbopackWatcher(): Promise<void> | void {
67
+ if (turbopackWatcher) {
68
+ const p = turbopackWatcher.close();
69
+ turbopackWatcher = null;
70
+ return p;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Higher-order Next.js configuration wrapper for Taser.js.
76
+ * Supports both Webpack and Turbopack dev/build flows.
77
+ */
78
+ export function createTaser(pluginOptions: NextTaserOptions = {}) {
79
+ return function withTaser<TNextConfig extends Record<string, any>>(
80
+ nextConfig: TNextConfig = {} as TNextConfig,
81
+ ): TNextConfig {
82
+ const cwd = pluginOptions.cwd ? resolve(pluginOptions.cwd) : process.cwd();
83
+ const nextPluginOptions = {
84
+ cwd,
85
+ ...(pluginOptions.config ? { config: pluginOptions.config } : {}),
86
+ };
87
+
88
+ // 1. Trigger initial generation
89
+ runNextTaserGeneration(cwd, nextPluginOptions).catch((err) => {
90
+ console.error(`[taserjs/next] Route generation error: ${err.message}`);
91
+ });
92
+
93
+ // 2. Start watcher for Turbopack dev mode if active or NODE_ENV != production
94
+ if (process.env.NODE_ENV !== "production") {
95
+ startTurbopackWatcher(cwd, nextPluginOptions).catch(() => {});
96
+ }
97
+
98
+ const enhancedConfig: any = {
99
+ ...nextConfig,
100
+ turbopack: nextConfig.turbopack ?? {},
101
+ webpack(config: any, webpackOptions: any) {
102
+ // Run generation during Webpack compile
103
+ const webpackPlugin = taserPlugin.webpack(nextPluginOptions);
104
+ config.plugins = config.plugins || [];
105
+ config.plugins.push(webpackPlugin);
106
+
107
+ if (typeof nextConfig.webpack === "function") {
108
+ return nextConfig.webpack(config, webpackOptions);
109
+ }
110
+ return config;
111
+ },
112
+ };
113
+
114
+ return enhancedConfig;
115
+ };
116
+ }
117
+
118
+ export const withTaser = (nextConfig: any, options?: NextTaserOptions) =>
119
+ createTaser(options)(nextConfig);
120
+
121
+ export default createTaser;