@vistagenic/vista 0.2.2 → 0.2.4

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 (42) hide show
  1. package/bin/vista.js +30 -20
  2. package/dist/bin/build-rsc.js +81 -5
  3. package/dist/bin/build.js +25 -5
  4. package/dist/bin/generate.d.ts +7 -0
  5. package/dist/bin/generate.js +248 -0
  6. package/dist/build/manifest.js +23 -5
  7. package/dist/client/link.d.ts +1 -1
  8. package/dist/client/link.js +30 -11
  9. package/dist/config.d.ts +19 -0
  10. package/dist/config.js +62 -4
  11. package/dist/server/engine.js +23 -57
  12. package/dist/server/rsc-engine.js +179 -119
  13. package/dist/server/rsc-upstream.js +24 -19
  14. package/dist/server/static-generator.js +98 -0
  15. package/dist/server/structure-validator.js +1 -1
  16. package/dist/server/typed-api-runtime.d.ts +16 -0
  17. package/dist/server/typed-api-runtime.js +336 -0
  18. package/dist/stack/client/create-client.d.ts +2 -0
  19. package/dist/stack/client/create-client.js +195 -0
  20. package/dist/stack/client/error.d.ts +18 -0
  21. package/dist/stack/client/error.js +22 -0
  22. package/dist/stack/client/index.d.ts +9 -0
  23. package/dist/stack/client/index.js +13 -0
  24. package/dist/stack/client/types.d.ts +39 -0
  25. package/dist/stack/client/types.js +2 -0
  26. package/dist/stack/index.d.ts +32 -0
  27. package/dist/stack/index.js +45 -0
  28. package/dist/stack/server/executor.d.ts +36 -0
  29. package/dist/stack/server/executor.js +174 -0
  30. package/dist/stack/server/index.d.ts +10 -0
  31. package/dist/stack/server/index.js +23 -0
  32. package/dist/stack/server/merge-routers.d.ts +2 -0
  33. package/dist/stack/server/merge-routers.js +80 -0
  34. package/dist/stack/server/procedure.d.ts +18 -0
  35. package/dist/stack/server/procedure.js +58 -0
  36. package/dist/stack/server/router.d.ts +9 -0
  37. package/dist/stack/server/router.js +80 -0
  38. package/dist/stack/server/serialization.d.ts +9 -0
  39. package/dist/stack/server/serialization.js +119 -0
  40. package/dist/stack/server/types.d.ts +100 -0
  41. package/dist/stack/server/types.js +2 -0
  42. package/package.json +11 -2
package/bin/vista.js CHANGED
@@ -1,12 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const command = process.argv[2];
4
- const flags = process.argv.slice(3);
5
-
6
- // RSC is the default mode (like Next.js App Router)
7
- // Use --legacy to fall back to traditional SSR mode
8
- const useLegacy = flags.includes('--legacy') || process.env.VISTA_LEGACY === 'true';
9
- const useRSC = !useLegacy;
3
+ const command = process.argv[2];
4
+ const flags = process.argv.slice(3);
5
+
6
+ if (command === 'g' || command === 'generate') {
7
+ const { runGenerateCommand } = require('../dist/bin/generate');
8
+ runGenerateCommand(flags).then((code) => {
9
+ if (code !== 0) process.exit(code);
10
+ });
11
+ return;
12
+ }
13
+
14
+ // RSC is the default mode (like Next.js App Router)
15
+ // Use --legacy to fall back to traditional SSR mode
16
+ const useLegacy = flags.includes('--legacy') || process.env.VISTA_LEGACY === 'true';
17
+ const useRSC = !useLegacy;
10
18
 
11
19
  // Mark startup time for "Ready in Xms" display
12
20
  const { markStartTime } = require('../dist/server/logger');
@@ -85,16 +93,18 @@ if (command === 'dev') {
85
93
  console.log('Usage: vista <command> [options]');
86
94
  console.log('');
87
95
  console.log('Commands:');
88
- console.log(' dev Start development server with HMR');
89
- console.log(' build Create production build');
90
- console.log(' start Start production server');
91
- console.log('');
92
- console.log('Options:');
93
- console.log(' --legacy Use traditional SSR mode (instead of RSC)');
94
- console.log('');
95
- console.log('Examples:');
96
- console.log(' vista dev # Start dev server (RSC mode)');
97
- console.log(' vista dev --legacy # Start dev server with legacy SSR');
98
- console.log(' vista build # Production build with RSC');
99
- console.log('');
100
- }
96
+ console.log(' dev Start development server with HMR');
97
+ console.log(' build Create production build');
98
+ console.log(' start Start production server');
99
+ console.log(' g Generate typed API scaffolds (api-init, router, procedure)');
100
+ console.log('');
101
+ console.log('Options:');
102
+ console.log(' --legacy Use traditional SSR mode (instead of RSC)');
103
+ console.log('');
104
+ console.log('Examples:');
105
+ console.log(' vista dev # Start dev server (RSC mode)');
106
+ console.log(' vista dev --legacy # Start dev server with legacy SSR');
107
+ console.log(' vista build # Production build with RSC');
108
+ console.log(' vista g api-init # Generate typed API starter files');
109
+ console.log('');
110
+ }
@@ -50,6 +50,53 @@ function runPostCSS(cwd, vistaDir) {
50
50
  }
51
51
  }
52
52
  }
53
+ function hasUseClientDirective(filePath) {
54
+ try {
55
+ const source = fs_1.default.readFileSync(filePath, 'utf-8');
56
+ return /^\s*['"]use client['"]\s*;?/m.test(source);
57
+ }
58
+ catch {
59
+ return false;
60
+ }
61
+ }
62
+ function collectUseClientFiles(dir, collected) {
63
+ if (!fs_1.default.existsSync(dir))
64
+ return;
65
+ const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
66
+ for (const entry of entries) {
67
+ const absolutePath = path_1.default.join(dir, entry.name);
68
+ if (entry.isDirectory()) {
69
+ collectUseClientFiles(absolutePath, collected);
70
+ continue;
71
+ }
72
+ if (!entry.isFile() || !entry.name.endsWith('.js')) {
73
+ continue;
74
+ }
75
+ if (hasUseClientDirective(absolutePath)) {
76
+ collected.add(path_1.default.resolve(absolutePath));
77
+ }
78
+ }
79
+ }
80
+ function resolvePackageRoot(cwd, packageName) {
81
+ try {
82
+ return path_1.default.dirname(require.resolve(`${packageName}/package.json`, { paths: [cwd] }));
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ }
88
+ function collectFrameworkClientReferences(cwd) {
89
+ const roots = [resolvePackageRoot(cwd, 'vista'), resolvePackageRoot(cwd, '@vistagenic/vista')].filter((value) => Boolean(value));
90
+ if (roots.length === 0) {
91
+ return [];
92
+ }
93
+ const collected = new Set();
94
+ for (const packageRoot of roots) {
95
+ collectUseClientFiles(path_1.default.join(packageRoot, 'dist', 'client'), collected);
96
+ collectUseClientFiles(path_1.default.join(packageRoot, 'dist', 'components'), collected);
97
+ }
98
+ return Array.from(collected);
99
+ }
53
100
  /**
54
101
  * Generate the RSC-aware client entry file
55
102
  */
@@ -236,6 +283,15 @@ async function buildRSC(watch = false) {
236
283
  }
237
284
  }
238
285
  }
286
+ // Include framework-level client boundaries (e.g. vista/link) so external
287
+ // package client modules resolve in React Flight manifests.
288
+ const frameworkClientReferences = collectFrameworkClientReferences(cwd);
289
+ if (frameworkClientReferences.length > 0) {
290
+ clientReferenceFiles = Array.from(new Set([...clientReferenceFiles, ...frameworkClientReferences]));
291
+ if (_debug) {
292
+ console.log(`[Vista JS RSC] Added ${frameworkClientReferences.length} framework client references`);
293
+ }
294
+ }
239
295
  // Generate manifests
240
296
  if (_debug)
241
297
  console.log('[vista:build] Generating manifests...');
@@ -301,13 +357,33 @@ async function buildRSC(watch = false) {
301
357
  const clientConfig = (0, compiler_1.createClientWebpackConfig)(options);
302
358
  const clientCompiler = (0, webpack_1.default)(clientConfig);
303
359
  syncReactServerManifests(vistaDirs.root);
304
- // Watch for CSS changes
360
+ // Watch for CSS + source changes that can affect Tailwind output.
305
361
  try {
306
362
  const chokidar = require('chokidar');
307
- chokidar.watch(path_1.default.join(cwd, 'app/**/*.css'), { ignoreInitial: true }).on('change', () => {
308
- if (_debug)
309
- console.log('[Vista JS RSC] CSS changed, rebuilding...');
310
- runPostCSS(cwd, vistaDirs.root);
363
+ const styleWatchRoots = ['app', 'components', 'content', 'lib', 'ctx', 'data']
364
+ .map((entry) => path_1.default.join(cwd, entry))
365
+ .filter((entry) => fs_1.default.existsSync(entry));
366
+ let cssTimer = null;
367
+ const scheduleCSSBuild = () => {
368
+ if (cssTimer)
369
+ clearTimeout(cssTimer);
370
+ cssTimer = setTimeout(() => {
371
+ if (_debug)
372
+ console.log('[Vista JS RSC] Style source changed, rebuilding CSS...');
373
+ runPostCSS(cwd, vistaDirs.root);
374
+ }, 120);
375
+ };
376
+ chokidar
377
+ .watch(styleWatchRoots, {
378
+ ignoreInitial: true,
379
+ ignored: (watchedPath) => watchedPath.includes(`${path_1.default.sep}node_modules${path_1.default.sep}`) ||
380
+ watchedPath.includes(`${path_1.default.sep}.git${path_1.default.sep}`) ||
381
+ watchedPath.includes(`${path_1.default.sep}.vista${path_1.default.sep}`),
382
+ })
383
+ .on('all', (_event, changedPath) => {
384
+ if (/\.(?:css|[cm]?[jt]sx?|md|mdx)$/i.test(changedPath)) {
385
+ scheduleCSSBuild();
386
+ }
311
387
  });
312
388
  }
313
389
  catch (e) {
package/dist/bin/build.js CHANGED
@@ -380,13 +380,33 @@ async function buildClient(watch = false, onRebuild) {
380
380
  // In watch mode, we return the compiler for use with dev middleware
381
381
  // Initial CSS build
382
382
  runPostCSS(cwd, vistaDir);
383
- // Watch for CSS changes separately (simple approach)
383
+ // Watch CSS + source files that may affect Tailwind output.
384
384
  const chokidar = require('chokidar');
385
385
  try {
386
- chokidar.watch(path_1.default.join(cwd, 'app/**/*.css'), { ignoreInitial: true }).on('change', () => {
387
- if (_debug)
388
- console.log('CSS changed, rebuilding...');
389
- runPostCSS(cwd, vistaDir);
386
+ const styleWatchRoots = ['app', 'components', 'content', 'lib', 'ctx', 'data']
387
+ .map((entry) => path_1.default.join(cwd, entry))
388
+ .filter((entry) => fs_1.default.existsSync(entry));
389
+ let cssTimer = null;
390
+ const scheduleCSSBuild = () => {
391
+ if (cssTimer)
392
+ clearTimeout(cssTimer);
393
+ cssTimer = setTimeout(() => {
394
+ if (_debug)
395
+ console.log('Style source changed, rebuilding CSS...');
396
+ runPostCSS(cwd, vistaDir);
397
+ }, 120);
398
+ };
399
+ chokidar
400
+ .watch(styleWatchRoots, {
401
+ ignoreInitial: true,
402
+ ignored: (watchedPath) => watchedPath.includes(`${path_1.default.sep}node_modules${path_1.default.sep}`) ||
403
+ watchedPath.includes(`${path_1.default.sep}.git${path_1.default.sep}`) ||
404
+ watchedPath.includes(`${path_1.default.sep}.vista${path_1.default.sep}`),
405
+ })
406
+ .on('all', (_event, changedPath) => {
407
+ if (/\.(?:css|[cm]?[jt]sx?|md|mdx)$/i.test(changedPath)) {
408
+ scheduleCSSBuild();
409
+ }
390
410
  });
391
411
  }
392
412
  catch (e) {
@@ -0,0 +1,7 @@
1
+ interface RunGenerateOptions {
2
+ cwd?: string;
3
+ log?: (message: string) => void;
4
+ error?: (message: string) => void;
5
+ }
6
+ export declare function runGenerateCommand(args: string[], options?: RunGenerateOptions): Promise<number>;
7
+ export {};
@@ -0,0 +1,248 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runGenerateCommand = runGenerateCommand;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ function toKebabCase(value) {
10
+ return value
11
+ .trim()
12
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
13
+ .replace(/[^a-zA-Z0-9]+/g, '-')
14
+ .replace(/^-+|-+$/g, '')
15
+ .toLowerCase();
16
+ }
17
+ function toPascalCase(value) {
18
+ return toKebabCase(value)
19
+ .split('-')
20
+ .filter(Boolean)
21
+ .map((segment) => segment[0].toUpperCase() + segment.slice(1))
22
+ .join('');
23
+ }
24
+ function toCamelCase(value) {
25
+ const pascal = toPascalCase(value);
26
+ return pascal ? pascal[0].toLowerCase() + pascal.slice(1) : '';
27
+ }
28
+ function ensureDirectory(targetDir) {
29
+ fs_1.default.mkdirSync(targetDir, { recursive: true });
30
+ }
31
+ function writeFileIfMissing(baseDir, relativePath, content) {
32
+ const absolutePath = path_1.default.join(baseDir, relativePath);
33
+ if (fs_1.default.existsSync(absolutePath)) {
34
+ return { path: absolutePath, created: false };
35
+ }
36
+ ensureDirectory(path_1.default.dirname(absolutePath));
37
+ fs_1.default.writeFileSync(absolutePath, content, 'utf8');
38
+ return { path: absolutePath, created: true };
39
+ }
40
+ function findMatchingBrace(source, openBraceIndex) {
41
+ let depth = 0;
42
+ for (let i = openBraceIndex; i < source.length; i++) {
43
+ const char = source[i];
44
+ if (char === '{')
45
+ depth++;
46
+ if (char === '}')
47
+ depth--;
48
+ if (depth === 0)
49
+ return i;
50
+ }
51
+ return -1;
52
+ }
53
+ function insertTypedApiConfigIntoObject(source, objectStartIndex) {
54
+ const openBraceIndex = source.indexOf('{', objectStartIndex);
55
+ if (openBraceIndex < 0) {
56
+ return null;
57
+ }
58
+ const closeBraceIndex = findMatchingBrace(source, openBraceIndex);
59
+ if (closeBraceIndex < 0) {
60
+ return null;
61
+ }
62
+ const before = source.slice(0, closeBraceIndex);
63
+ const after = source.slice(closeBraceIndex);
64
+ const insertion = `\n experimental: {\n typedApi: {\n enabled: true,\n },\n },`;
65
+ return `${before}${insertion}${after}`;
66
+ }
67
+ function ensureTypedApiEnabledInConfig(cwd) {
68
+ const tsPath = path_1.default.join(cwd, 'vista.config.ts');
69
+ const jsPath = path_1.default.join(cwd, 'vista.config.js');
70
+ if (!fs_1.default.existsSync(tsPath) && !fs_1.default.existsSync(jsPath)) {
71
+ const configSource = [
72
+ 'const config = {',
73
+ ' experimental: {',
74
+ ' typedApi: {',
75
+ ' enabled: true,',
76
+ ' },',
77
+ ' },',
78
+ '};',
79
+ '',
80
+ 'export default config;',
81
+ '',
82
+ ].join('\n');
83
+ fs_1.default.writeFileSync(tsPath, configSource, 'utf8');
84
+ return 'created';
85
+ }
86
+ const targetPath = fs_1.default.existsSync(tsPath) ? tsPath : jsPath;
87
+ const source = fs_1.default.readFileSync(targetPath, 'utf8');
88
+ if (/\btypedApi\b/.test(source) && /\benabled\s*:\s*true\b/.test(source)) {
89
+ return 'unchanged';
90
+ }
91
+ const constConfigIndex = source.indexOf('const config');
92
+ if (constConfigIndex >= 0) {
93
+ const updated = insertTypedApiConfigIntoObject(source, constConfigIndex);
94
+ if (updated) {
95
+ fs_1.default.writeFileSync(targetPath, updated, 'utf8');
96
+ return 'updated';
97
+ }
98
+ }
99
+ const exportDefaultIndex = source.indexOf('export default');
100
+ if (exportDefaultIndex >= 0) {
101
+ const updated = insertTypedApiConfigIntoObject(source, exportDefaultIndex);
102
+ if (updated) {
103
+ fs_1.default.writeFileSync(targetPath, updated, 'utf8');
104
+ return 'updated';
105
+ }
106
+ }
107
+ return 'manual';
108
+ }
109
+ function renderApiInitEntrypoint() {
110
+ return [
111
+ "import { vstack } from 'vista/stack';",
112
+ "import { createRootRouter } from './routers';",
113
+ '',
114
+ 'const v = vstack.init();',
115
+ '',
116
+ 'export const router = createRootRouter(v);',
117
+ '',
118
+ ].join('\n');
119
+ }
120
+ function renderRootRouter() {
121
+ return [
122
+ "import type { VStackInstance } from 'vista/stack';",
123
+ "import { healthProcedure } from '../procedures/health';",
124
+ '',
125
+ 'export function createRootRouter(v: VStackInstance<any, any>) {',
126
+ ' return v.router({',
127
+ ' health: healthProcedure(v),',
128
+ ' });',
129
+ '}',
130
+ '',
131
+ ].join('\n');
132
+ }
133
+ function renderProcedure(name, method) {
134
+ const safeName = toCamelCase(name);
135
+ const functionName = `${safeName}Procedure`;
136
+ const procedureMethod = method === 'post' ? 'mutation' : 'query';
137
+ const sampleResult = method === 'post'
138
+ ? " ok: true,\n message: 'Mutation executed',"
139
+ : " ok: true,\n message: 'Query executed',";
140
+ return [
141
+ "import type { VStackInstance } from 'vista/stack';",
142
+ '',
143
+ `export function ${functionName}(v: VStackInstance<any, any>) {`,
144
+ ` return v.procedure.${procedureMethod}(() => ({`,
145
+ sampleResult,
146
+ ' }));',
147
+ '}',
148
+ '',
149
+ ].join('\n');
150
+ }
151
+ function renderRouter(name) {
152
+ const pascal = toPascalCase(name);
153
+ const camel = toCamelCase(name);
154
+ return [
155
+ "import type { VStackInstance } from 'vista/stack';",
156
+ '',
157
+ `export function create${pascal}Router(v: VStackInstance<any, any>) {`,
158
+ ' return v.router({',
159
+ ` ${camel}: v.procedure.query(() => ({`,
160
+ ` route: '${toKebabCase(name)}',`,
161
+ " ok: true,",
162
+ ' })),',
163
+ ' });',
164
+ '}',
165
+ '',
166
+ ].join('\n');
167
+ }
168
+ function printGenerateUsage(log) {
169
+ log('Vista generator usage:');
170
+ log(' vista g api-init');
171
+ log(' vista g router <name>');
172
+ log(' vista g procedure <name> [get|post]');
173
+ }
174
+ async function runGenerateCommand(args, options = {}) {
175
+ const cwd = options.cwd ?? process.cwd();
176
+ const log = options.log ?? console.log;
177
+ const error = options.error ?? console.error;
178
+ const command = (args[0] || '').toLowerCase();
179
+ if (!command || !['api-init', 'router', 'procedure'].includes(command)) {
180
+ printGenerateUsage(log);
181
+ return 1;
182
+ }
183
+ if (command === 'api-init') {
184
+ const writes = [
185
+ writeFileIfMissing(cwd, path_1.default.join('app', 'api', 'typed.ts'), renderApiInitEntrypoint()),
186
+ writeFileIfMissing(cwd, path_1.default.join('app', 'api', 'routers', 'index.ts'), renderRootRouter()),
187
+ writeFileIfMissing(cwd, path_1.default.join('app', 'api', 'procedures', 'health.ts'), renderProcedure('health', 'get')),
188
+ ];
189
+ const configState = ensureTypedApiEnabledInConfig(cwd);
190
+ writes.forEach((result) => {
191
+ const relativePath = path_1.default.relative(cwd, result.path).replace(/\\/g, '/');
192
+ log(`${result.created ? 'created' : 'skipped'} ${relativePath}`);
193
+ });
194
+ if (configState === 'created') {
195
+ log('created vista.config.ts with experimental.typedApi.enabled = true');
196
+ }
197
+ else if (configState === 'updated') {
198
+ log('updated vista.config.* to enable experimental typed API');
199
+ }
200
+ else if (configState === 'unchanged') {
201
+ log('typed API config already enabled');
202
+ }
203
+ else {
204
+ error('Could not update vista.config automatically. Please enable experimental.typedApi.enabled manually.');
205
+ return 1;
206
+ }
207
+ return 0;
208
+ }
209
+ if (command === 'router') {
210
+ const rawName = args[1];
211
+ if (!rawName) {
212
+ error('Missing router name. Example: vista g router users');
213
+ return 1;
214
+ }
215
+ const safeName = toKebabCase(rawName);
216
+ if (!safeName) {
217
+ error(`Invalid router name "${rawName}".`);
218
+ return 1;
219
+ }
220
+ const result = writeFileIfMissing(cwd, path_1.default.join('app', 'api', 'routers', `${safeName}.ts`), renderRouter(safeName));
221
+ const relativePath = path_1.default.relative(cwd, result.path).replace(/\\/g, '/');
222
+ log(`${result.created ? 'created' : 'skipped'} ${relativePath}`);
223
+ return 0;
224
+ }
225
+ if (command === 'procedure') {
226
+ const rawName = args[1];
227
+ if (!rawName) {
228
+ error('Missing procedure name. Example: vista g procedure list-users get');
229
+ return 1;
230
+ }
231
+ const methodArg = (args[2] || 'get').toLowerCase();
232
+ if (methodArg !== 'get' && methodArg !== 'post') {
233
+ error(`Invalid procedure method "${methodArg}". Use "get" or "post".`);
234
+ return 1;
235
+ }
236
+ const safeName = toKebabCase(rawName);
237
+ if (!safeName) {
238
+ error(`Invalid procedure name "${rawName}".`);
239
+ return 1;
240
+ }
241
+ const result = writeFileIfMissing(cwd, path_1.default.join('app', 'api', 'procedures', `${safeName}.ts`), renderProcedure(safeName, methodArg));
242
+ const relativePath = path_1.default.relative(cwd, result.path).replace(/\\/g, '/');
243
+ log(`${result.created ? 'created' : 'skipped'} ${relativePath}`);
244
+ return 0;
245
+ }
246
+ printGenerateUsage(log);
247
+ return 1;
248
+ }
@@ -103,11 +103,29 @@ function generateBuildManifest(vistaDir, buildId, pages = {}) {
103
103
  return manifest;
104
104
  }
105
105
  function toRegexFromPattern(pattern) {
106
- const escaped = pattern
107
- .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
108
- .replace(/:([a-zA-Z0-9_]+)\*/g, '(?<$1>.+)')
109
- .replace(/:([a-zA-Z0-9_]+)/g, '(?<$1>[^/]+)');
110
- return `^${escaped}$`;
106
+ if (pattern === '/') {
107
+ return '^/$';
108
+ }
109
+ const normalized = pattern.startsWith('/') ? pattern.slice(1) : pattern;
110
+ const parts = normalized.split('/').filter(Boolean);
111
+ const regexParts = parts.map((part) => {
112
+ if (!part.startsWith(':')) {
113
+ return part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
114
+ }
115
+ const dynamicMatch = /^:([a-zA-Z0-9_]+)(\*)?(\?)?$/.exec(part);
116
+ if (!dynamicMatch) {
117
+ return part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
118
+ }
119
+ const [, paramName, isCatchAll, isOptional] = dynamicMatch;
120
+ if (isCatchAll && isOptional) {
121
+ return `(?<${paramName}>.*)`;
122
+ }
123
+ if (isCatchAll) {
124
+ return `(?<${paramName}>.+)`;
125
+ }
126
+ return `(?<${paramName}>[^/]+)`;
127
+ });
128
+ return `^/${regexParts.join('/')}$`;
111
129
  }
112
130
  function toRouteInfo(route) {
113
131
  return {
@@ -17,7 +17,7 @@ export interface LinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>,
17
17
  shallow?: boolean;
18
18
  /** Force href on child element */
19
19
  passHref?: boolean;
20
- /** Prefetch strategy: true = viewport+hover, 'auto' = hover-only, false/null = off */
20
+ /** Prefetch strategy: true = always, 'auto' = production-only (Next-like), false/null = off */
21
21
  prefetch?: boolean | 'auto' | null;
22
22
  /** Locale for internationalised routing */
23
23
  locale?: string | false;
@@ -102,16 +102,31 @@ function isInternalUrl(url) {
102
102
  }
103
103
  return true;
104
104
  }
105
- exports.Link = react_1.default.forwardRef(({ href, as, replace, scroll = true, shallow, passHref, prefetch = true, legacyBehavior, children, onClick, onMouseEnter, onTouchStart, onNavigate, target, ...props }, ref) => {
105
+ function resolvePrefetchBehavior(prefetch) {
106
+ if (prefetch === false || prefetch === null) {
107
+ return { viewport: false, intent: false };
108
+ }
109
+ if (prefetch === true) {
110
+ return { viewport: true, intent: true };
111
+ }
112
+ const isProduction = process.env.NODE_ENV === 'production';
113
+ return {
114
+ viewport: isProduction,
115
+ intent: isProduction,
116
+ };
117
+ }
118
+ exports.Link = react_1.default.forwardRef(({ href, as, replace, scroll = true, shallow, passHref, prefetch = 'auto', legacyBehavior, children, onClick, onMouseEnter, onTouchStart, onNavigate, target, ...props }, ref) => {
106
119
  // Try the RSC router first — if we're inside an RSCRouter, use
107
120
  // Flight-based navigation. Otherwise fall back to the legacy router.
108
- const rscRouter = (0, rsc_router_1.useRSCRouter)();
109
- const legacyRouter = (0, router_1.useRouter)();
110
- const pathname = rscRouter ? rscRouter.pathname : (0, router_1.usePathname)();
121
+ const rscRouter = (0, react_1.useContext)(rsc_router_1.RSCRouterContext);
122
+ const legacyRouter = (0, react_1.useContext)(router_1.RouterContext);
123
+ const fallbackPathname = (0, router_1.usePathname)();
124
+ const pathname = rscRouter?.pathname ?? legacyRouter?.pathname ?? fallbackPathname;
111
125
  const linkRef = (0, react_1.useRef)(null);
112
126
  const targetPath = formatUrl(as || href);
113
127
  const [isActive, setIsActive] = (0, react_1.useState)(false);
114
128
  const internal = (0, react_1.useMemo)(() => isInternalUrl(targetPath), [targetPath]);
129
+ const prefetchBehavior = (0, react_1.useMemo)(() => resolvePrefetchBehavior(prefetch), [prefetch]);
115
130
  // Combine refs
116
131
  const setRefs = (0, react_1.useCallback)((node) => {
117
132
  linkRef.current = node;
@@ -133,12 +148,14 @@ exports.Link = react_1.default.forwardRef(({ href, as, replace, scroll = true, s
133
148
  }, [targetPath, pathname]);
134
149
  // Prefetch on viewport intersection (skip for external links & auto mode)
135
150
  (0, react_1.useEffect)(() => {
136
- if (!prefetch || prefetch === null || prefetch === 'auto')
151
+ if (!prefetchBehavior.viewport)
137
152
  return;
138
153
  if (!internal)
139
154
  return;
140
155
  if (typeof window === 'undefined')
141
156
  return;
157
+ if (pathname === targetPath)
158
+ return;
142
159
  const element = linkRef.current;
143
160
  if (!element)
144
161
  return;
@@ -160,12 +177,12 @@ exports.Link = react_1.default.forwardRef(({ href, as, replace, scroll = true, s
160
177
  });
161
178
  observer.observe(element);
162
179
  return () => observer.disconnect();
163
- }, [prefetch, targetPath, rscRouter, internal]);
180
+ }, [prefetchBehavior.viewport, targetPath, pathname, rscRouter, internal]);
164
181
  // Prefetch on hover
165
182
  const handleMouseEnter = (0, react_1.useCallback)((e) => {
166
183
  if (onMouseEnter)
167
184
  onMouseEnter(e);
168
- if (prefetch !== false && prefetch !== null && internal) {
185
+ if (prefetchBehavior.intent && internal && pathname !== targetPath) {
169
186
  if (rscRouter) {
170
187
  rscRouter.prefetch(targetPath);
171
188
  }
@@ -173,12 +190,12 @@ exports.Link = react_1.default.forwardRef(({ href, as, replace, scroll = true, s
173
190
  prefetchUrl(targetPath);
174
191
  }
175
192
  }
176
- }, [onMouseEnter, prefetch, targetPath, rscRouter, internal]);
193
+ }, [onMouseEnter, prefetchBehavior.intent, targetPath, pathname, rscRouter, internal]);
177
194
  // Prefetch on touch (mobile devices)
178
195
  const handleTouchStart = (0, react_1.useCallback)((e) => {
179
196
  if (onTouchStart)
180
197
  onTouchStart(e);
181
- if (prefetch !== false && prefetch !== null && internal) {
198
+ if (prefetchBehavior.intent && internal && pathname !== targetPath) {
182
199
  if (rscRouter) {
183
200
  rscRouter.prefetch(targetPath);
184
201
  }
@@ -186,7 +203,7 @@ exports.Link = react_1.default.forwardRef(({ href, as, replace, scroll = true, s
186
203
  prefetchUrl(targetPath);
187
204
  }
188
205
  }
189
- }, [onTouchStart, prefetch, targetPath, rscRouter, internal]);
206
+ }, [onTouchStart, prefetchBehavior.intent, targetPath, pathname, rscRouter, internal]);
190
207
  // Handle navigation
191
208
  const handleClick = (0, react_1.useCallback)((e) => {
192
209
  if (onClick)
@@ -204,6 +221,8 @@ exports.Link = react_1.default.forwardRef(({ href, as, replace, scroll = true, s
204
221
  return;
205
222
  if (!internal)
206
223
  return; // external / mailto / tel
224
+ if (!rscRouter && !legacyRouter)
225
+ return; // No router provider -> allow native navigation
207
226
  e.preventDefault();
208
227
  if (onNavigate)
209
228
  onNavigate();
@@ -216,7 +235,7 @@ exports.Link = react_1.default.forwardRef(({ href, as, replace, scroll = true, s
216
235
  rscRouter.push(targetPath, { scroll });
217
236
  }
218
237
  }
219
- else {
238
+ else if (legacyRouter) {
220
239
  if (replace) {
221
240
  legacyRouter.replace(targetPath, { scroll });
222
241
  }
package/dist/config.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { ImageConfig } from './image/image-config';
2
2
  export type ValidationMode = 'strict' | 'warn';
3
3
  export type ValidationLogLevel = 'compact' | 'verbose';
4
+ export type TypedApiSerialization = 'json' | 'superjson';
4
5
  export interface StructureValidationConfig {
5
6
  /** Enable structure validation. Default: true */
6
7
  enabled?: boolean;
@@ -13,6 +14,17 @@ export interface StructureValidationConfig {
13
14
  /** Debounce interval for watch events in ms. Default: 120 */
14
15
  watchDebounceMs?: number;
15
16
  }
17
+ export interface TypedApiExperimentalConfig {
18
+ /** Enable typed API runtime. Default: false */
19
+ enabled?: boolean;
20
+ /** Request/response serialization mode. Default: 'json' */
21
+ serialization?: TypedApiSerialization;
22
+ /** Maximum request body size for typed API endpoints in bytes. Default: 1MB */
23
+ bodySizeLimitBytes?: number;
24
+ }
25
+ export interface ExperimentalConfig {
26
+ typedApi?: TypedApiExperimentalConfig;
27
+ }
16
28
  export interface VistaConfig {
17
29
  images?: ImageConfig;
18
30
  react?: any;
@@ -22,11 +34,18 @@ export interface VistaConfig {
22
34
  validation?: {
23
35
  structure?: StructureValidationConfig;
24
36
  };
37
+ experimental?: ExperimentalConfig;
25
38
  }
26
39
  export declare const defaultStructureValidationConfig: Required<StructureValidationConfig>;
40
+ export declare const defaultTypedApiConfig: Required<TypedApiExperimentalConfig>;
27
41
  export declare const defaultConfig: VistaConfig;
28
42
  /**
29
43
  * Resolve the effective structure validation config merging user overrides.
30
44
  */
31
45
  export declare function resolveStructureValidationConfig(config: VistaConfig): Required<StructureValidationConfig>;
46
+ export type ResolvedTypedApiConfig = Required<TypedApiExperimentalConfig>;
47
+ /**
48
+ * Resolve and sanitize experimental typed API config.
49
+ */
50
+ export declare function resolveTypedApiConfig(config: VistaConfig): ResolvedTypedApiConfig;
32
51
  export declare function loadConfig(cwd?: string): VistaConfig;