@vistagenic/vista 0.2.3 → 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.
@@ -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) {
@@ -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
  }
@@ -114,6 +114,27 @@ function resolvePort(raw, fallback) {
114
114
  }
115
115
  return value;
116
116
  }
117
+ function normalizeModuleCachePath(filePath) {
118
+ return filePath.replace(/\\/g, '/').toLowerCase();
119
+ }
120
+ function shouldInvalidateDevModule(modulePath, cwd) {
121
+ const normalized = normalizeModuleCachePath(modulePath);
122
+ const rootPrefix = normalizeModuleCachePath(`${cwd}${path_1.default.sep}`);
123
+ if (!normalized.startsWith(rootPrefix))
124
+ return false;
125
+ if (normalized.includes('/node_modules/'))
126
+ return false;
127
+ if (normalized.includes(`/${constants_1.BUILD_DIR.toLowerCase()}/`))
128
+ return false;
129
+ return /\.(?:[cm]?[jt]sx?|json)$/i.test(normalized);
130
+ }
131
+ function clearProjectRequireCache(cwd) {
132
+ for (const key of Object.keys(require.cache)) {
133
+ if (!shouldInvalidateDevModule(key, cwd))
134
+ continue;
135
+ delete require.cache[key];
136
+ }
137
+ }
117
138
  function resolveFromWorkspace(specifier, cwd) {
118
139
  const searchRoots = [
119
140
  cwd,
@@ -241,18 +262,23 @@ function cleanHotUpdateFiles(cwd) {
241
262
  }
242
263
  }
243
264
  }
244
- function findChunkFiles(cwd) {
265
+ function findChunkFiles(cwd, isDev) {
245
266
  const chunksDir = path_1.default.join(cwd, constants_1.BUILD_DIR, 'static', 'chunks');
246
267
  if (!fs_1.default.existsSync(chunksDir))
247
268
  return [];
248
269
  const files = fs_1.default
249
270
  .readdirSync(chunksDir)
250
271
  .filter((name) => name.endsWith('.js') && !name.endsWith('.map') && !name.includes('.hot-update.'));
272
+ // In dev, avoid loading stale production artifacts left from a previous build.
273
+ // Production chunks end with a hash suffix like `main-1a2b3c4d.js`.
274
+ const normalizedFiles = isDev
275
+ ? files.filter((name) => !/-[0-9a-f]{8,}\.js$/i.test(name))
276
+ : files;
251
277
  // Load webpack runtime first, then framework, then the rest alphabetically.
252
278
  // This ensures the chunk registry (__webpack_require__) is available before
253
279
  // any deferred chunk tries to self-register.
254
280
  const priority = ['webpack.js', 'framework.js', 'vendor.js'];
255
- return files.sort((a, b) => {
281
+ return normalizedFiles.sort((a, b) => {
256
282
  const ai = priority.indexOf(a);
257
283
  const bi = priority.indexOf(b);
258
284
  if (ai !== -1 && bi !== -1)
@@ -264,6 +290,41 @@ function findChunkFiles(cwd) {
264
290
  return a.localeCompare(b);
265
291
  });
266
292
  }
293
+ function normalizeSSRManifest(manifest) {
294
+ if (!manifest || !manifest.moduleMap) {
295
+ return manifest;
296
+ }
297
+ const moduleMap = manifest.moduleMap;
298
+ const aliasEntries = [];
299
+ for (const [moduleKey, exportsMap] of Object.entries(moduleMap)) {
300
+ const normalizedExports = {};
301
+ for (const [exportName, rawEntry] of Object.entries(exportsMap || {})) {
302
+ const entry = rawEntry || {};
303
+ const normalizedId = String(entry.id ?? entry.specifier ?? moduleKey);
304
+ normalizedExports[exportName] = {
305
+ id: normalizedId,
306
+ chunks: Array.isArray(entry.chunks) ? entry.chunks : [],
307
+ name: entry.name || exportName,
308
+ };
309
+ }
310
+ moduleMap[moduleKey] = normalizedExports;
311
+ aliasEntries.push([moduleKey, normalizedExports]);
312
+ for (const normalizedEntry of Object.values(normalizedExports)) {
313
+ const aliasKey = String(normalizedEntry.id);
314
+ aliasEntries.push([aliasKey, normalizedExports]);
315
+ }
316
+ }
317
+ for (const [aliasKey, exportsMap] of aliasEntries) {
318
+ if (!moduleMap[aliasKey]) {
319
+ moduleMap[aliasKey] = exportsMap;
320
+ }
321
+ }
322
+ return manifest;
323
+ }
324
+ function loadSSRManifestFromDisk(absolutePath) {
325
+ const manifest = JSON.parse(fs_1.default.readFileSync(absolutePath, 'utf-8'));
326
+ return normalizeSSRManifest(manifest);
327
+ }
267
328
  function matchPattern(pathname, pattern) {
268
329
  const patternParts = pattern.split('/').filter(Boolean);
269
330
  const pathParts = pathname.split('/').filter(Boolean);
@@ -326,14 +387,10 @@ function extractParams(pathname, route) {
326
387
  }
327
388
  return params;
328
389
  }
329
- async function createRouteElement(route, context, isDev, rootLayout) {
390
+ async function createRouteElement(route, context, isDev, rootLayout, cwd) {
330
391
  const { params, searchParams, req } = context;
331
392
  if (isDev) {
332
- for (const key of Object.keys(require.cache)) {
333
- if (key.includes(`${path_1.default.sep}app${path_1.default.sep}`)) {
334
- delete require.cache[key];
335
- }
336
- }
393
+ clearProjectRequireCache(cwd);
337
394
  }
338
395
  const PageModule = require(route.pagePath);
339
396
  const PageComponent = PageModule.default;
@@ -520,11 +577,21 @@ async function renderFlightToHTMLStream(upstreamOrigin, pathname, search, metada
520
577
  const nodeStream = stream_1.Readable.fromWeb(upstream.body);
521
578
  // 3. Decode Flight stream into a React tree
522
579
  const flightResponse = flightSSRClient.createFromNodeStream(nodeStream, ssrManifest);
523
- // 4. Wrap in a component that consumes the Flight response
524
- function FlightRoot() {
525
- return react_1.default.use(flightResponse);
580
+ let element;
581
+ // In dev, decode the flight payload before React.use() render path.
582
+ // This keeps manifest mismatch errors in the request try/catch boundary
583
+ // instead of crashing the whole process with an uncaught exception.
584
+ if (isDev) {
585
+ const resolvedTree = await flightResponse;
586
+ element = react_1.default.createElement(react_1.default.Fragment, null, resolvedTree);
587
+ }
588
+ else {
589
+ // 4. Wrap in a component that consumes the Flight response
590
+ function FlightRoot() {
591
+ return react_1.default.use(flightResponse);
592
+ }
593
+ element = react_1.default.createElement(FlightRoot);
526
594
  }
527
- const element = react_1.default.createElement(FlightRoot);
528
595
  // 5. Build script tags for client chunks
529
596
  const scripts = chunkFiles
530
597
  .map((chunk) => `<script defer src="${constants_1.STATIC_CHUNKS_PATH}${chunk}"></script>`)
@@ -669,26 +736,7 @@ function startRSCServer(options = {}) {
669
736
  ? ssrManifestLegacyPath
670
737
  : null;
671
738
  if (resolvedSSRManifestPath) {
672
- ssrManifest = JSON.parse(fs_1.default.readFileSync(resolvedSSRManifestPath, 'utf-8'));
673
- // Normalise SSR manifest entries: ReactFlightWebpackPlugin generates
674
- // {specifier, name} but react-server-dom-webpack/client.node expects
675
- // {id, chunks, name}. Our VistaSSRManifestPatch webpack plugin handles
676
- // this at build time, but in dev mode the manifest may be re-read from
677
- // disk before the patch runs. Belt-and-suspenders fix:
678
- if (ssrManifest && ssrManifest.moduleMap) {
679
- const moduleMap = ssrManifest.moduleMap;
680
- for (const exports of Object.values(moduleMap)) {
681
- for (const [exportName, entry] of Object.entries(exports)) {
682
- if (entry.specifier && !entry.id) {
683
- exports[exportName] = {
684
- id: entry.specifier,
685
- chunks: [],
686
- name: entry.name || exportName,
687
- };
688
- }
689
- }
690
- }
691
- }
739
+ ssrManifest = loadSSRManifestFromDisk(resolvedSSRManifestPath);
692
740
  }
693
741
  else if (flightSSRClient) {
694
742
  // Can't use Flight SSR without the manifest
@@ -814,21 +862,71 @@ function startRSCServer(options = {}) {
814
862
  const pushSSE = (payload) => {
815
863
  sseReloadClients.forEach((c) => c.write(`data: ${payload}\n\n`));
816
864
  };
817
- // Watch app/ directory — server component changes need a full page reload
818
- // because they run on the upstream RSC process (which invalidates
819
- // require.cache per-request, so a fresh fetch returns new content).
820
- const appDir = path_1.default.join(cwd, 'app');
865
+ const watchExtPattern = /\.(?:[cm]?[jt]sx?|css|md|mdx|json)$/i;
866
+ const watchRoots = [
867
+ 'app',
868
+ 'components',
869
+ 'content',
870
+ 'lib',
871
+ 'ctx',
872
+ 'data',
873
+ 'middleware.ts',
874
+ 'vista.config.ts',
875
+ 'content-collections.ts',
876
+ ]
877
+ .map((entry) => path_1.default.join(cwd, entry))
878
+ .filter((entry) => fs_1.default.existsSync(entry));
821
879
  let reloadTimer = null;
822
- fsWatcher = fs_1.default.watch(appDir, { recursive: true }, (_event, filename) => {
823
- if (filename && /\.[jt]sx?$/.test(filename)) {
824
- if (reloadTimer)
825
- clearTimeout(reloadTimer);
826
- reloadTimer = setTimeout(() => {
827
- (0, logger_1.logEvent)('File changed, reloading...');
828
- pushSSE('reload');
829
- }, 120);
880
+ const scheduleReload = () => {
881
+ if (reloadTimer)
882
+ clearTimeout(reloadTimer);
883
+ reloadTimer = setTimeout(() => {
884
+ (0, logger_1.logEvent)('Source changed, reloading...');
885
+ pushSSE('reload');
886
+ }, 140);
887
+ };
888
+ try {
889
+ const chokidar = require('chokidar');
890
+ const watcher = chokidar.watch(watchRoots, {
891
+ ignoreInitial: true,
892
+ ignored: (watchedPath) => watchedPath.includes(`${path_1.default.sep}node_modules${path_1.default.sep}`) ||
893
+ watchedPath.includes(`${path_1.default.sep}.git${path_1.default.sep}`) ||
894
+ watchedPath.includes(`${path_1.default.sep}${constants_1.BUILD_DIR}${path_1.default.sep}`),
895
+ });
896
+ watcher.on('all', (_event, filePath) => {
897
+ if (filePath && watchExtPattern.test(filePath)) {
898
+ scheduleReload();
899
+ }
900
+ });
901
+ fsWatcher = watcher;
902
+ }
903
+ catch {
904
+ const nativeWatchers = [];
905
+ const onChange = (_event, filePath) => {
906
+ if (filePath && watchExtPattern.test(filePath)) {
907
+ scheduleReload();
908
+ }
909
+ };
910
+ for (const watchPath of watchRoots) {
911
+ try {
912
+ const stat = fs_1.default.statSync(watchPath);
913
+ if (stat.isDirectory()) {
914
+ nativeWatchers.push(fs_1.default.watch(watchPath, { recursive: true }, onChange));
915
+ }
916
+ else {
917
+ nativeWatchers.push(fs_1.default.watch(watchPath, onChange));
918
+ }
919
+ }
920
+ catch {
921
+ // Skip missing or unsupported watch path.
922
+ }
830
923
  }
831
- });
924
+ fsWatcher = {
925
+ close: () => {
926
+ nativeWatchers.forEach((watcher) => watcher.close());
927
+ },
928
+ };
929
+ }
832
930
  }
833
931
  if (isDev && options.compiler) {
834
932
  app.use((0, webpack_dev_middleware_1.default)(options.compiler, {
@@ -856,22 +954,7 @@ function startRSCServer(options = {}) {
856
954
  }
857
955
  // Reload SSR manifest on rebuild too
858
956
  if (resolvedSSRManifestPath && fs_1.default.existsSync(resolvedSSRManifestPath)) {
859
- ssrManifest = JSON.parse(fs_1.default.readFileSync(resolvedSSRManifestPath, 'utf-8'));
860
- // Normalise {specifier,name} → {id,chunks,name} (same as initial load)
861
- if (ssrManifest && ssrManifest.moduleMap) {
862
- const mm = ssrManifest.moduleMap;
863
- for (const exports of Object.values(mm)) {
864
- for (const [expName, entry] of Object.entries(exports)) {
865
- if (entry.specifier && !entry.id) {
866
- exports[expName] = {
867
- id: entry.specifier,
868
- chunks: [],
869
- name: entry.name || expName,
870
- };
871
- }
872
- }
873
- }
874
- }
957
+ ssrManifest = loadSSRManifestFromDisk(resolvedSSRManifestPath);
875
958
  }
876
959
  });
877
960
  }
@@ -1082,17 +1165,21 @@ function startRSCServer(options = {}) {
1082
1165
  // ==================================================================
1083
1166
  if (useFlightSSR) {
1084
1167
  try {
1168
+ if (isDev && resolvedSSRManifestPath && fs_1.default.existsSync(resolvedSSRManifestPath)) {
1169
+ try {
1170
+ ssrManifest = loadSSRManifestFromDisk(resolvedSSRManifestPath);
1171
+ }
1172
+ catch {
1173
+ // Manifest may be mid-write during compilation; keep the last good in-memory copy.
1174
+ }
1175
+ }
1085
1176
  // Metadata extraction: still done locally so we have <head> content
1086
1177
  const rootLayout = (0, root_resolver_1.resolveRootLayout)(cwd, isDev);
1087
1178
  const route = matchRoute(req.path, serverManifest.routes);
1088
1179
  let metadataHtml = '';
1089
1180
  if (route) {
1090
1181
  if (isDev) {
1091
- for (const key of Object.keys(require.cache)) {
1092
- if (key.includes(`${path_1.default.sep}app${path_1.default.sep}`)) {
1093
- delete require.cache[key];
1094
- }
1095
- }
1182
+ clearProjectRequireCache(cwd);
1096
1183
  }
1097
1184
  const PageModule = require(route.pagePath);
1098
1185
  let metadata = { ...(rootLayout.metadata || {}) };
@@ -1114,7 +1201,7 @@ function startRSCServer(options = {}) {
1114
1201
  metadataHtml = metadata ? generateMetadataHtml(metadata) : '';
1115
1202
  }
1116
1203
  // Render the page via Flight stream → SSR
1117
- await renderFlightToHTMLStream(upstreamOrigin, req.path, req.query ? new URLSearchParams(req.query).toString() : '', metadataHtml, findChunkFiles(cwd), rootLayout.mode, flightSSRClient, ssrManifest, res, isDev);
1204
+ await renderFlightToHTMLStream(upstreamOrigin, req.path, req.query ? new URLSearchParams(req.query).toString() : '', metadataHtml, findChunkFiles(cwd, isDev), rootLayout.mode, flightSSRClient, ssrManifest, res, isDev);
1118
1205
  return;
1119
1206
  }
1120
1207
  catch (flightError) {
@@ -1182,7 +1269,7 @@ function startRSCServer(options = {}) {
1182
1269
  res
1183
1270
  .status(404)
1184
1271
  .type('text/html')
1185
- .send(createHtmlDocument(html, '', findChunkFiles(cwd), rootLayout.mode));
1272
+ .send(createHtmlDocument(html, '', findChunkFiles(cwd, isDev), rootLayout.mode));
1186
1273
  return;
1187
1274
  }
1188
1275
  res.status(404).type('text/html').send((0, not_found_page_1.getStyledNotFoundHTML)());
@@ -1190,14 +1277,14 @@ function startRSCServer(options = {}) {
1190
1277
  }
1191
1278
  const params = extractParams(req.path, route);
1192
1279
  const searchParams = Object.fromEntries(new URLSearchParams(req.query).entries());
1193
- const { element, metadata, rootMode } = await createRouteElement(route, { params, searchParams, req }, isDev, rootLayout);
1280
+ const { element, metadata, rootMode } = await createRouteElement(route, { params, searchParams, req }, isDev, rootLayout, cwd);
1194
1281
  const appHtml = (0, server_1.renderToString)(element);
1195
1282
  const { generateMetadataHtml } = require('../metadata/generate');
1196
1283
  const metadataHtml = metadata ? generateMetadataHtml(metadata) : '';
1197
1284
  res
1198
1285
  .status(200)
1199
1286
  .type('text/html')
1200
- .send(createHtmlDocument(appHtml, metadataHtml, findChunkFiles(cwd), rootMode));
1287
+ .send(createHtmlDocument(appHtml, metadataHtml, findChunkFiles(cwd, isDev), rootMode));
1201
1288
  }
1202
1289
  catch (error) {
1203
1290
  if (error?.name === 'NotFoundError') {
@@ -1214,7 +1301,7 @@ function startRSCServer(options = {}) {
1214
1301
  res
1215
1302
  .status(404)
1216
1303
  .type('text/html')
1217
- .send(createHtmlDocument(html, '', findChunkFiles(cwd), rootLayout.mode));
1304
+ .send(createHtmlDocument(html, '', findChunkFiles(cwd, isDev), rootLayout.mode));
1218
1305
  return;
1219
1306
  }
1220
1307
  res.status(404).type('text/html').send((0, not_found_page_1.getStyledNotFoundHTML)());
@@ -77,6 +77,25 @@ function resolveFromWorkspace(specifier, cwd) {
77
77
  function normalizeModulePath(filePath) {
78
78
  return filePath.replace(/\\/g, '/').toLowerCase();
79
79
  }
80
+ function shouldInvalidateDevModule(modulePath, cwd) {
81
+ const normalized = normalizeModulePath(modulePath);
82
+ const rootPrefix = normalizeModulePath(`${cwd}${path_1.default.sep}`);
83
+ if (!normalized.startsWith(rootPrefix))
84
+ return false;
85
+ if (normalized.includes('/node_modules/'))
86
+ return false;
87
+ if (normalized.includes(`/${constants_1.BUILD_DIR.toLowerCase()}/`))
88
+ return false;
89
+ return /\.(?:[cm]?[jt]sx?|json)$/i.test(normalized);
90
+ }
91
+ function clearProjectRequireCache(cwd) {
92
+ for (const key of Object.keys(require.cache)) {
93
+ if (!shouldInvalidateDevModule(key, cwd))
94
+ continue;
95
+ delete require.cache[key];
96
+ clientDirectiveCache.delete(key);
97
+ }
98
+ }
80
99
  function setupTypeScriptRuntime(cwd) {
81
100
  try {
82
101
  const swcPath = require.resolve('@swc-node/register', { paths: [cwd] });
@@ -174,18 +193,10 @@ function installSingleReactResolution() {
174
193
  function installClientLoadHook(cwd, createClientModuleProxy) {
175
194
  if (installedClientLoadHook)
176
195
  return;
177
- const appDir = path_1.default.join(cwd, 'app');
178
- const componentsDir = path_1.default.join(cwd, 'components');
179
- const normalizedAppDir = normalizeModulePath(appDir);
180
- const normalizedComponentsDir = normalizeModulePath(componentsDir);
181
196
  originalCompile = CjsModule.prototype._compile;
182
197
  CjsModule.prototype._compile = function (content, filename) {
183
- const normalized = normalizeModulePath(filename);
184
- const isInAppTree = normalized.startsWith(normalizedAppDir);
185
- const isInComponentsTree = normalized.startsWith(normalizedComponentsDir);
186
- if ((isInAppTree || isInComponentsTree) &&
187
- /\.[jt]sx?$/.test(filename) &&
188
- isClientBoundaryFile(filename, content)) {
198
+ const isJavaScriptModule = /\.[jt]sx?$/.test(filename);
199
+ if (isJavaScriptModule && isClientBoundaryFile(filename, content)) {
189
200
  const moduleId = (0, url_1.pathToFileURL)(filename).href;
190
201
  this.exports = createClientModuleProxy(moduleId);
191
202
  return;
@@ -254,16 +265,10 @@ function extractParams(pathname, route) {
254
265
  }
255
266
  return params;
256
267
  }
257
- async function createRouteElement(route, context, isDev) {
268
+ async function createRouteElement(route, context, isDev, cwd) {
258
269
  const { params, searchParams, req } = context;
259
270
  if (isDev) {
260
- for (const key of Object.keys(require.cache)) {
261
- if (key.includes(`${path_1.default.sep}app${path_1.default.sep}`) ||
262
- key.includes(`${path_1.default.sep}components${path_1.default.sep}`)) {
263
- delete require.cache[key];
264
- clientDirectiveCache.delete(key);
265
- }
266
- }
271
+ clearProjectRequireCache(cwd);
267
272
  }
268
273
  const PageModule = require(route.pagePath);
269
274
  const PageComponent = PageModule.default;
@@ -429,7 +434,7 @@ function startUpstream() {
429
434
  }
430
435
  const params = extractParams(pathname, route);
431
436
  const searchParams = Object.fromEntries(new URLSearchParams(req.query).entries());
432
- const element = await createRouteElement(route, { params, searchParams, req }, isDev);
437
+ const element = await createRouteElement(route, { params, searchParams, req }, isDev, cwd);
433
438
  res.setHeader('Content-Type', 'text/x-component');
434
439
  res.setHeader('Vary', 'Accept');
435
440
  const stream = flightServer.renderToPipeableStream(element, flightManifest, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vistagenic/vista",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "The React Framework for Visionaries - Rust-powered SSR with Server Components",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -133,5 +133,5 @@
133
133
  "@types/webpack-hot-middleware": "^2.25.9",
134
134
  "typescript": "^5.7.2"
135
135
  },
136
- "gitHead": "4a9f1db26023c01465b997f6955b3d3a57b6ac84"
136
+ "gitHead": "9fac2fe1ed23e51248ba8bcecbd623b9e3a3b520"
137
137
  }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- The MIT License (MIT)
2
-
3
- Copyright (c) 2026 Vista.js contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.