@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.
- package/bin/vista.js +30 -20
- package/dist/bin/build-rsc.js +81 -5
- package/dist/bin/build.js +25 -5
- package/dist/bin/generate.d.ts +7 -0
- package/dist/bin/generate.js +248 -0
- package/dist/build/manifest.js +23 -5
- package/dist/client/link.d.ts +1 -1
- package/dist/client/link.js +30 -11
- package/dist/config.d.ts +19 -0
- package/dist/config.js +62 -4
- package/dist/server/engine.js +23 -57
- package/dist/server/rsc-engine.js +179 -119
- package/dist/server/rsc-upstream.js +24 -19
- package/dist/server/static-generator.js +98 -0
- package/dist/server/structure-validator.js +1 -1
- package/dist/server/typed-api-runtime.d.ts +16 -0
- package/dist/server/typed-api-runtime.js +336 -0
- package/dist/stack/client/create-client.d.ts +2 -0
- package/dist/stack/client/create-client.js +195 -0
- package/dist/stack/client/error.d.ts +18 -0
- package/dist/stack/client/error.js +22 -0
- package/dist/stack/client/index.d.ts +9 -0
- package/dist/stack/client/index.js +13 -0
- package/dist/stack/client/types.d.ts +39 -0
- package/dist/stack/client/types.js +2 -0
- package/dist/stack/index.d.ts +32 -0
- package/dist/stack/index.js +45 -0
- package/dist/stack/server/executor.d.ts +36 -0
- package/dist/stack/server/executor.js +174 -0
- package/dist/stack/server/index.d.ts +10 -0
- package/dist/stack/server/index.js +23 -0
- package/dist/stack/server/merge-routers.d.ts +2 -0
- package/dist/stack/server/merge-routers.js +80 -0
- package/dist/stack/server/procedure.d.ts +18 -0
- package/dist/stack/server/procedure.js +58 -0
- package/dist/stack/server/router.d.ts +9 -0
- package/dist/stack/server/router.js +80 -0
- package/dist/stack/server/serialization.d.ts +9 -0
- package/dist/stack/server/serialization.js +119 -0
- package/dist/stack/server/types.d.ts +100 -0
- package/dist/stack/server/types.js +2 -0
- 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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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('
|
|
93
|
-
console.log('
|
|
94
|
-
console.log('');
|
|
95
|
-
console.log('
|
|
96
|
-
console.log('
|
|
97
|
-
console.log(' vista dev
|
|
98
|
-
console.log(' vista
|
|
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
|
+
}
|
package/dist/bin/build-rsc.js
CHANGED
|
@@ -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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
|
383
|
+
// Watch CSS + source files that may affect Tailwind output.
|
|
384
384
|
const chokidar = require('chokidar');
|
|
385
385
|
try {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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,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
|
+
}
|
package/dist/build/manifest.js
CHANGED
|
@@ -103,11 +103,29 @@ function generateBuildManifest(vistaDir, buildId, pages = {}) {
|
|
|
103
103
|
return manifest;
|
|
104
104
|
}
|
|
105
105
|
function toRegexFromPattern(pattern) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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 {
|
package/dist/client/link.d.ts
CHANGED
|
@@ -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 =
|
|
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;
|
package/dist/client/link.js
CHANGED
|
@@ -102,16 +102,31 @@ function isInternalUrl(url) {
|
|
|
102
102
|
}
|
|
103
103
|
return true;
|
|
104
104
|
}
|
|
105
|
-
|
|
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,
|
|
109
|
-
const legacyRouter = (0,
|
|
110
|
-
const
|
|
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 (!
|
|
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
|
-
}, [
|
|
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 (
|
|
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,
|
|
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 (
|
|
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,
|
|
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;
|