@vistagenic/vista 0.3.3 → 0.3.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 (35) hide show
  1. package/dist/bin/build-rsc.js +18 -0
  2. package/dist/bin/build.js +10 -0
  3. package/dist/bin/webpack.config.d.ts +3 -1
  4. package/dist/bin/webpack.config.js +3 -1
  5. package/dist/build/rsc/compiler.d.ts +3 -1
  6. package/dist/build/rsc/compiler.js +7 -3
  7. package/dist/build/rsc/react-client-reference-manifest.d.ts +7 -0
  8. package/dist/build/rsc/react-client-reference-manifest.js +132 -6
  9. package/dist/client/link.js +1 -1
  10. package/dist/client/navigation.js +2 -2
  11. package/dist/client/rsc-router.d.ts +1 -3
  12. package/dist/client/rsc-router.js +98 -45
  13. package/dist/deploy/adapters/cloudflare.js +2 -25
  14. package/dist/deploy/adapters/netlify.js +2 -14
  15. package/dist/deploy/utils.d.ts +6 -0
  16. package/dist/deploy/utils.js +64 -1
  17. package/dist/image/get-img-props.js +21 -11
  18. package/dist/image/image-config.d.ts +2 -0
  19. package/dist/image/image-config.js +17 -0
  20. package/dist/image/index.d.ts +18 -3
  21. package/dist/image/index.js +1 -1
  22. package/dist/image/react-server.d.ts +1 -2
  23. package/dist/image/react-server.js +1 -1
  24. package/dist/server/app-router-runtime.d.ts +1 -0
  25. package/dist/server/app-router-runtime.js +34 -9
  26. package/dist/server/hydration-chunks.d.ts +7 -0
  27. package/dist/server/hydration-chunks.js +49 -0
  28. package/dist/server/module-compile-hook.js +11 -0
  29. package/dist/server/rsc-engine.js +14 -61
  30. package/dist/server/rsc-upstream.js +10 -3
  31. package/dist/server/ssr-webpack-shim.d.ts +6 -0
  32. package/dist/server/ssr-webpack-shim.js +29 -0
  33. package/dist/server/static-generator.d.ts +2 -0
  34. package/dist/server/static-generator.js +29 -29
  35. package/package.json +1 -1
@@ -244,6 +244,14 @@ waitForInlineFlightPayload(4000).then(function (inlineFlight) {
244
244
  return;
245
245
  }
246
246
 
247
+ if (/(?:^|\\n)\\d+:E\\{/.test(inlineFlight)) {
248
+ reportDevRuntimeError(
249
+ 'Hydration Error',
250
+ new Error('Inline Flight payload contains error rows. Keeping server HTML instead of hydrating the document.')
251
+ );
252
+ return;
253
+ }
254
+
247
255
  const initialResponse = createFromReadableStream(flightTextToReadableStream(inlineFlight), {
248
256
  callServer,
249
257
  }) as Promise<React.ReactNode>;
@@ -365,6 +373,14 @@ async function buildRSC(watch = false) {
365
373
  const engineVariant = (0, config_1.resolveAndApplyEngineVariant)(vistaConfig);
366
374
  const structureConfig = (0, config_1.resolveStructureValidationConfig)(vistaConfig);
367
375
  const cacheComponentsConfig = (0, config_1.resolveCacheComponentsConfig)(vistaConfig);
376
+ const deployConfig = (0, config_1.resolveDeployConfig)(vistaConfig);
377
+ const imagesUnoptimized = vistaConfig.images?.unoptimized === true || deployConfig.output === 'static';
378
+ if (imagesUnoptimized) {
379
+ process.env.VISTA_IMAGES_UNOPTIMIZED = '1';
380
+ }
381
+ if (deployConfig.output) {
382
+ process.env.VISTA_DEPLOY_OUTPUT = deployConfig.output;
383
+ }
368
384
  if (_debug)
369
385
  console.log(`[vista:build] Engine variant: ${engineVariant}`);
370
386
  if (structureConfig.enabled) {
@@ -487,6 +503,8 @@ async function buildRSC(watch = false) {
487
503
  buildId,
488
504
  engineVariant,
489
505
  clientReferenceFiles,
506
+ imagesUnoptimized,
507
+ deployOutput: deployConfig.output,
490
508
  };
491
509
  // Build CSS
492
510
  runPostCSS(cwd, vistaDirs.root);
package/dist/bin/build.js CHANGED
@@ -279,6 +279,14 @@ async function buildClient(watch = false, onRebuild) {
279
279
  const engineVariant = (0, config_1.resolveAndApplyEngineVariant)(vistaConfig);
280
280
  const structureConfig = (0, config_1.resolveStructureValidationConfig)(vistaConfig);
281
281
  const cacheComponentsConfig = (0, config_1.resolveCacheComponentsConfig)(vistaConfig);
282
+ const deployConfig = (0, config_1.resolveDeployConfig)(vistaConfig);
283
+ const imagesUnoptimized = vistaConfig.images?.unoptimized === true || deployConfig.output === 'static';
284
+ if (imagesUnoptimized) {
285
+ process.env.VISTA_IMAGES_UNOPTIMIZED = '1';
286
+ }
287
+ if (deployConfig.output) {
288
+ process.env.VISTA_DEPLOY_OUTPUT = deployConfig.output;
289
+ }
282
290
  if (_debug)
283
291
  console.log(`[vista:build] Engine variant: ${engineVariant}`);
284
292
  if (structureConfig.enabled) {
@@ -383,6 +391,8 @@ async function buildClient(watch = false, onRebuild) {
383
391
  isDev: watch,
384
392
  engineVariant,
385
393
  cacheComponentsEnabled: cacheComponentsConfig.enabled,
394
+ imagesUnoptimized,
395
+ deployOutput: deployConfig.output,
386
396
  });
387
397
  // Create Webpack compiler
388
398
  exports.compiler = compiler = (0, webpack_1.default)(config);
@@ -1,9 +1,11 @@
1
1
  import webpack from 'webpack';
2
- import type { VistaEngineVariant } from '../config';
2
+ import type { DeployOutput, VistaEngineVariant } from '../config';
3
3
  export interface WebpackConfigOptions {
4
4
  cwd: string;
5
5
  isDev: boolean;
6
6
  engineVariant?: VistaEngineVariant;
7
7
  cacheComponentsEnabled?: boolean;
8
+ imagesUnoptimized?: boolean;
9
+ deployOutput?: DeployOutput;
8
10
  }
9
11
  export declare function createWebpackConfig(options: WebpackConfigOptions): webpack.Configuration;
@@ -13,7 +13,7 @@ const vista_flight_plugin_1 = require("../build/webpack/plugins/vista-flight-plu
13
13
  const constants_1 = require("../constants");
14
14
  const app_dir_1 = require("../server/app-dir");
15
15
  function createWebpackConfig(options) {
16
- const { cwd, isDev, engineVariant = 'default', cacheComponentsEnabled = false } = options;
16
+ const { cwd, isDev, engineVariant = 'default', cacheComponentsEnabled = false, imagesUnoptimized = false, deployOutput, } = options;
17
17
  const vistaDir = path_1.default.join(cwd, constants_1.BUILD_DIR);
18
18
  const flashDir = path_1.default.join(cwd, constants_1.FLASH_DIR);
19
19
  const entryPoint = path_1.default.join(vistaDir, 'client.tsx');
@@ -166,6 +166,8 @@ function createWebpackConfig(options) {
166
166
  'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production'),
167
167
  'process.env.VISTA_ENGINE': JSON.stringify(engineVariant),
168
168
  'process.env.VISTA_ENGINE_VARIANT': JSON.stringify(engineVariant),
169
+ 'process.env.VISTA_IMAGES_UNOPTIMIZED': JSON.stringify(imagesUnoptimized ? '1' : ''),
170
+ 'process.env.VISTA_DEPLOY_OUTPUT': JSON.stringify(deployOutput || ''),
169
171
  }),
170
172
  new mini_css_extract_plugin_1.default({
171
173
  filename: isDev ? 'modules.css' : 'modules-[contenthash:8].css',
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import webpack from 'webpack';
9
9
  import { VistaDirs } from '../manifest';
10
- import type { VistaEngineVariant } from '../../config';
10
+ import type { DeployOutput, VistaEngineVariant } from '../../config';
11
11
  export interface RSCCompilerOptions {
12
12
  cwd: string;
13
13
  isDev: boolean;
@@ -15,6 +15,8 @@ export interface RSCCompilerOptions {
15
15
  buildId: string;
16
16
  engineVariant?: VistaEngineVariant;
17
17
  clientReferenceFiles?: string[];
18
+ imagesUnoptimized?: boolean;
19
+ deployOutput?: DeployOutput;
18
20
  }
19
21
  /**
20
22
  * Create Server-Side Webpack Configuration
@@ -61,7 +61,7 @@ function resolveFromWorkspace(specifier, cwd) {
61
61
  * Output goes to .vista/server/ and is NEVER sent to the client.
62
62
  */
63
63
  function createServerWebpackConfig(options) {
64
- const { cwd, isDev, vistaDirs, buildId, engineVariant = 'default' } = options;
64
+ const { cwd, isDev, vistaDirs, buildId, engineVariant = 'default', imagesUnoptimized = false, deployOutput, } = options;
65
65
  const swcLoaderPath = resolveFromWorkspace('swc-loader', cwd);
66
66
  const nullLoaderPath = resolveFromWorkspace('null-loader', cwd);
67
67
  const cssLoaderPath = resolveFromWorkspace('css-loader', cwd);
@@ -181,6 +181,8 @@ function createServerWebpackConfig(options) {
181
181
  'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production'),
182
182
  'process.env.VISTA_ENGINE': JSON.stringify(engineVariant),
183
183
  'process.env.VISTA_ENGINE_VARIANT': JSON.stringify(engineVariant),
184
+ 'process.env.VISTA_IMAGES_UNOPTIMIZED': JSON.stringify(imagesUnoptimized ? '1' : ''),
185
+ 'process.env.VISTA_DEPLOY_OUTPUT': JSON.stringify(deployOutput || ''),
184
186
  [constants_1.BUILD_ID_DEFINE]: JSON.stringify(buildId),
185
187
  [constants_1.SERVER_DEFINE]: 'true',
186
188
  }),
@@ -196,7 +198,7 @@ function createServerWebpackConfig(options) {
196
198
  * Server components are replaced with client references.
197
199
  */
198
200
  function createClientWebpackConfig(options) {
199
- const { cwd, isDev, vistaDirs, buildId, engineVariant = 'default', clientReferenceFiles = [] } = options;
201
+ const { cwd, isDev, vistaDirs, buildId, engineVariant = 'default', clientReferenceFiles = [], imagesUnoptimized = false, deployOutput, } = options;
200
202
  const swcLoaderPath = resolveFromWorkspace('swc-loader', cwd);
201
203
  const nullLoaderPath = resolveFromWorkspace('null-loader', cwd);
202
204
  const cssLoaderPath = resolveFromWorkspace('css-loader', cwd);
@@ -222,7 +224,7 @@ function createClientWebpackConfig(options) {
222
224
  entry: clientEntry,
223
225
  output: {
224
226
  path: vistaDirs.chunks,
225
- filename: isDev ? '[name].js' : 'main-[contenthash:8].js',
227
+ filename: isDev ? '[name].js' : '[name]-[contenthash:8].js',
226
228
  chunkFilename: isDev ? '[name].js' : '[name]-[contenthash:8].js',
227
229
  publicPath: constants_1.STATIC_CHUNKS_PATH,
228
230
  clean: !isDev,
@@ -405,6 +407,8 @@ function createClientWebpackConfig(options) {
405
407
  'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production'),
406
408
  'process.env.VISTA_ENGINE': JSON.stringify(engineVariant),
407
409
  'process.env.VISTA_ENGINE_VARIANT': JSON.stringify(engineVariant),
410
+ 'process.env.VISTA_IMAGES_UNOPTIMIZED': JSON.stringify(imagesUnoptimized ? '1' : ''),
411
+ 'process.env.VISTA_DEPLOY_OUTPUT': JSON.stringify(deployOutput || ''),
408
412
  [constants_1.BUILD_ID_DEFINE]: JSON.stringify(buildId),
409
413
  [constants_1.SERVER_DEFINE]: 'false',
410
414
  }),
@@ -18,6 +18,13 @@ export interface ReactServerConsumerManifest {
18
18
  moduleMap?: Record<string, Record<string, ReactServerConsumerManifestEntry>>;
19
19
  serverModuleMap?: Record<string, unknown>;
20
20
  }
21
+ /**
22
+ * Flight encode looks up `file://...#ExportName`. Webpack may have keyed the
23
+ * same module under a standalone copy, a different drive-letter case, or a
24
+ * slightly different absolute prefix. Resolve those aliases at lookup time so
25
+ * missing keys do not serialize as Flight `E{"digest":""}` rows.
26
+ */
27
+ export declare function resolveReactClientManifestEntry(manifest: ReactClientReferenceManifest, key: string): ReactClientReferenceManifestEntry | undefined;
21
28
  export declare function normalizeReactClientReferenceManifest(input: ReactClientReferenceManifest): ReactClientReferenceManifest;
22
29
  export declare function normalizeReactServerConsumerManifest(input: ReactServerConsumerManifest): ReactServerConsumerManifest;
23
30
  /**
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveReactClientManifestEntry = resolveReactClientManifestEntry;
6
7
  exports.normalizeReactClientReferenceManifest = normalizeReactClientReferenceManifest;
7
8
  exports.normalizeReactServerConsumerManifest = normalizeReactServerConsumerManifest;
8
9
  exports.createGuardedReactClientManifest = createGuardedReactClientManifest;
@@ -73,19 +74,36 @@ function addDriveLetterVariants(specifier, variants) {
73
74
  variants.add(specifier.replace(/^file:\/\/\/([A-Za-z]):/, `file:///${lowerDrive}:`));
74
75
  variants.add(specifier.replace(/^file:\/\/\/([A-Za-z]):/, `file:///${upperDrive}:`));
75
76
  }
77
+ function addStandalonePathVariants(specifier, variants) {
78
+ addDriveLetterVariants(specifier, variants);
79
+ const standaloneProject = '/.vista/standalone/project';
80
+ const standaloneRuntime = '/.vista/standalone/runtime';
81
+ if (specifier.includes(standaloneProject)) {
82
+ addDriveLetterVariants(specifier.split(standaloneProject).join(''), variants);
83
+ }
84
+ if (specifier.includes(standaloneRuntime)) {
85
+ addDriveLetterVariants(specifier.split(standaloneRuntime).join(''), variants);
86
+ }
87
+ if (!specifier.includes('/.vista/standalone/') && specifier.startsWith('file://')) {
88
+ const inserted = specifier.replace(/^(file:\/\/\/(?:[A-Za-z]:)?(?:\/[^/]+)*)(\/(?:app|components|utils|lib|src|content|hooks)\/)/i, `$1${standaloneProject}$2`);
89
+ if (inserted !== specifier) {
90
+ addDriveLetterVariants(inserted, variants);
91
+ }
92
+ }
93
+ }
76
94
  function buildSpecifierVariants(specifier) {
77
95
  const baseSpecifier = specifier.split('#', 1)[0];
78
96
  const variants = new Set();
79
- addDriveLetterVariants(baseSpecifier, variants);
97
+ addStandalonePathVariants(baseSpecifier, variants);
80
98
  if (baseSpecifier.startsWith('file://')) {
81
99
  try {
82
- addDriveLetterVariants(decodeURI(baseSpecifier), variants);
100
+ addStandalonePathVariants(decodeURI(baseSpecifier), variants);
83
101
  }
84
102
  catch {
85
103
  // ignore decode failures
86
104
  }
87
105
  try {
88
- addDriveLetterVariants((0, url_1.pathToFileURL)((0, url_1.fileURLToPath)(baseSpecifier)).toString(), variants);
106
+ addStandalonePathVariants((0, url_1.pathToFileURL)((0, url_1.fileURLToPath)(baseSpecifier)).toString(), variants);
89
107
  }
90
108
  catch {
91
109
  // ignore encode failures
@@ -93,6 +111,106 @@ function buildSpecifierVariants(specifier) {
93
111
  }
94
112
  return Array.from(variants);
95
113
  }
114
+ function splitManifestKey(key) {
115
+ const hashIdx = key.lastIndexOf('#');
116
+ if (hashIdx <= 0) {
117
+ return { specifier: key, exportName: null };
118
+ }
119
+ return {
120
+ specifier: key.slice(0, hashIdx),
121
+ exportName: key.slice(hashIdx + 1),
122
+ };
123
+ }
124
+ function normalizeManifestSpecifier(specifier) {
125
+ return specifier
126
+ .replace(/\\/g, '/')
127
+ .replace(/^file:\/\//i, '')
128
+ .replace(/^\/+/, '')
129
+ .replace(/\/\.vista\/standalone\/(?:project|runtime)/gi, '')
130
+ .toLowerCase();
131
+ }
132
+ function relativeClientPath(normalized) {
133
+ const markers = [
134
+ '/app/',
135
+ '/components/',
136
+ '/utils/',
137
+ '/lib/',
138
+ '/src/',
139
+ '/content/',
140
+ '/hooks/',
141
+ '/packages/vista/',
142
+ ];
143
+ let best = normalized;
144
+ for (const marker of markers) {
145
+ const idx = normalized.lastIndexOf(marker);
146
+ if (idx !== -1) {
147
+ const sliced = normalized.slice(idx);
148
+ if (sliced.length <= best.length) {
149
+ best = sliced;
150
+ }
151
+ }
152
+ }
153
+ return best.replace(/\.(tsx|ts|jsx|js|mjs|cjs)$/i, '');
154
+ }
155
+ function exportNamesCompatible(requested, existing) {
156
+ if (requested === null || existing === null)
157
+ return true;
158
+ if (requested === existing)
159
+ return true;
160
+ const aliases = new Set(['', '*', 'default']);
161
+ return aliases.has(requested) && aliases.has(existing);
162
+ }
163
+ /**
164
+ * Flight encode looks up `file://...#ExportName`. Webpack may have keyed the
165
+ * same module under a standalone copy, a different drive-letter case, or a
166
+ * slightly different absolute prefix. Resolve those aliases at lookup time so
167
+ * missing keys do not serialize as Flight `E{"digest":""}` rows.
168
+ */
169
+ function resolveReactClientManifestEntry(manifest, key) {
170
+ if (Object.prototype.hasOwnProperty.call(manifest, key)) {
171
+ return manifest[key];
172
+ }
173
+ const { specifier, exportName } = splitManifestKey(key);
174
+ const candidates = [];
175
+ for (const variant of buildSpecifierVariants(specifier)) {
176
+ candidates.push(variant);
177
+ if (exportName !== null) {
178
+ candidates.push(`${variant}#${exportName}`);
179
+ candidates.push(`${variant}#`);
180
+ candidates.push(`${variant}#default`);
181
+ candidates.push(`${variant}#*`);
182
+ }
183
+ }
184
+ for (const candidate of candidates) {
185
+ if (Object.prototype.hasOwnProperty.call(manifest, candidate)) {
186
+ return manifest[candidate];
187
+ }
188
+ }
189
+ const needle = normalizeManifestSpecifier(specifier);
190
+ const needleFile = needle.split('/').pop() || needle;
191
+ let fuzzy;
192
+ for (const [existingKey, entry] of Object.entries(manifest)) {
193
+ if (!entry || typeof entry !== 'object')
194
+ continue;
195
+ const existing = splitManifestKey(existingKey);
196
+ const existingNorm = normalizeManifestSpecifier(existing.specifier);
197
+ const existingFile = existingNorm.split('/').pop() || existingNorm;
198
+ const existingStem = existingFile.replace(/\.(tsx|ts|jsx|js|mjs|cjs)$/i, '');
199
+ const needleStem = needleFile.replace(/\.(tsx|ts|jsx|js|mjs|cjs)$/i, '');
200
+ const genericStem = existingStem === 'index' || existingStem === 'page' || existingStem === 'layout';
201
+ const pathMatches = existingNorm === needle ||
202
+ relativeClientPath(existingNorm) === relativeClientPath(needle) ||
203
+ (Boolean(needleFile) && !genericStem && existingStem === needleStem);
204
+ if (!pathMatches || !exportNamesCompatible(exportName, existing.exportName)) {
205
+ continue;
206
+ }
207
+ if (existing.exportName === exportName) {
208
+ return entry;
209
+ }
210
+ fuzzy = entry;
211
+ }
212
+ return fuzzy;
213
+ }
96
214
  function createAliasedEntry(entry, exportName) {
97
215
  if (exportName === '') {
98
216
  return {
@@ -226,9 +344,11 @@ function normalizeReactServerConsumerManifest(input) {
226
344
  function createGuardedReactClientManifest(manifest) {
227
345
  return new Proxy(manifest, {
228
346
  get(target, prop, receiver) {
229
- if (typeof prop === 'string' &&
230
- (prop.startsWith('file:') || prop.includes('#')) &&
231
- !Object.prototype.hasOwnProperty.call(target, prop)) {
347
+ if (typeof prop === 'string' && (prop.startsWith('file:') || prop.includes('#'))) {
348
+ const resolved = resolveReactClientManifestEntry(target, prop);
349
+ if (resolved) {
350
+ return resolved;
351
+ }
232
352
  const fileHint = prop.split('#')[0];
233
353
  throw new Error(`Could not find the module "${prop}" in the React Client Manifest. ` +
234
354
  `Client Components need a 'use client' directive and must live under a scanned project directory ` +
@@ -236,5 +356,11 @@ function createGuardedReactClientManifest(manifest) {
236
356
  }
237
357
  return Reflect.get(target, prop, receiver);
238
358
  },
359
+ has(target, prop) {
360
+ if (typeof prop === 'string' && (prop.startsWith('file:') || prop.includes('#'))) {
361
+ return Boolean(resolveReactClientManifestEntry(target, prop));
362
+ }
363
+ return Reflect.has(target, prop);
364
+ },
239
365
  });
240
366
  }
@@ -118,7 +118,7 @@ function resolvePrefetchBehavior(prefetch) {
118
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) => {
119
119
  // Try the RSC router first — if we're inside an RSCRouter, use
120
120
  // Flight-based navigation. Otherwise fall back to the legacy router.
121
- const rscRouter = (0, react_1.useContext)(rsc_router_1.RSCRouterContext);
121
+ const rscRouter = (0, rsc_router_1.useRSCRouter)();
122
122
  const legacyRouter = (0, react_1.useContext)(router_1.RouterContext);
123
123
  const fallbackPathname = (0, router_1.usePathname)();
124
124
  const pathname = rscRouter?.pathname ?? legacyRouter?.pathname ?? fallbackPathname;
@@ -59,7 +59,7 @@ const rsc_router_1 = require("./rsc-router");
59
59
  * to popstate events.
60
60
  */
61
61
  function usePathname() {
62
- const rscCtx = React.useContext(rsc_router_1.RSCRouterContext);
62
+ const rscCtx = (0, rsc_router_1.useRSCRouter)();
63
63
  if (rscCtx)
64
64
  return rscCtx.pathname;
65
65
  const [pathname, setPathname] = React.useState(() => typeof window !== 'undefined' ? window.location.pathname : '/');
@@ -77,7 +77,7 @@ function usePathname() {
77
77
  * Uses the RSC router context when available.
78
78
  */
79
79
  function useSearchParams() {
80
- const rscCtx = React.useContext(rsc_router_1.RSCRouterContext);
80
+ const rscCtx = (0, rsc_router_1.useRSCRouter)();
81
81
  if (rscCtx)
82
82
  return rscCtx.searchParams;
83
83
  const [searchParams, setSearchParams] = React.useState(() => typeof window !== 'undefined'
@@ -57,9 +57,7 @@ declare global {
57
57
  at: number;
58
58
  };
59
59
  };
60
- __VISTA_RSC_ROUTER__?: {
61
- refresh: () => void;
62
- prefetch: (url: string) => void;
60
+ __VISTA_RSC_ROUTER__?: RSCNavigationState & {
63
61
  resume: (url: string) => void;
64
62
  getState: () => {
65
63
  pathname: string;
@@ -121,14 +121,36 @@ function getCallServer() {
121
121
  }
122
122
  return _callServer;
123
123
  }
124
- function fetchFlight(pathname, search) {
124
+ function flightRequestUrl(pathname, search) {
125
+ const normalized = pathname === '/' ? '' : pathname.replace(/\/$/, '');
126
+ // Static CDNs rewrite `/rsc/docs` → `/rsc/docs.rsc`. Request the extensionless
127
+ // URL so that rewrite is not applied twice (`/rsc/docs.rsc` → `/rsc/docs.rsc.rsc`).
128
+ return `/rsc${normalized}${search}`;
129
+ }
130
+ function hardNavigate(url) {
131
+ if (typeof window === 'undefined')
132
+ return;
133
+ window.location.assign(url);
134
+ }
135
+ function fetchFlight(pathname, search, options = {}) {
125
136
  const key = cacheKey(pathname, search);
126
137
  const cached = flightCache.get(key);
127
138
  if (cached)
128
139
  return cached;
129
140
  const create = getCreateFromFetch();
130
- const thenable = create(fetch(`/rsc${pathname}${search}`, {
141
+ const requestUrl = flightRequestUrl(pathname, search);
142
+ const hardFallback = options.hardFallback !== false;
143
+ const thenable = create(fetch(requestUrl, {
131
144
  headers: { Accept: 'text/x-component' },
145
+ }).then((response) => {
146
+ const contentType = response.headers.get('content-type') || '';
147
+ if (!response.ok || contentType.includes('text/html')) {
148
+ if (hardFallback) {
149
+ hardNavigate(`${pathname}${search}`);
150
+ }
151
+ throw new Error(`Flight request failed (${response.status}): ${requestUrl}`);
152
+ }
153
+ return response;
132
154
  }), { callServer: getCallServer() });
133
155
  // Evict oldest if over limit
134
156
  if (flightCache.size >= CACHE_MAX) {
@@ -148,8 +170,7 @@ function prefetchFlight(pathname, search) {
148
170
  const key = cacheKey(pathname, search);
149
171
  if (flightCache.has(key))
150
172
  return;
151
- // Create the thenable which kicks off the fetch
152
- fetchFlight(pathname, search);
173
+ fetchFlight(pathname, search, { hardFallback: false });
153
174
  }
154
175
  // ---------------------------------------------------------------------------
155
176
  // RSCRoot — reads the current Flight thenable
@@ -157,6 +178,26 @@ function prefetchFlight(pathname, search) {
157
178
  function RSCRoot({ response }) {
158
179
  return React.use(response);
159
180
  }
181
+ class FlightNavigationErrorBoundary extends React.Component {
182
+ state = { failed: false };
183
+ static getDerivedStateFromError() {
184
+ return { failed: true };
185
+ }
186
+ componentDidCatch() {
187
+ hardNavigate(this.props.href);
188
+ }
189
+ componentDidUpdate(prevProps) {
190
+ if (prevProps.href !== this.props.href && this.state.failed) {
191
+ this.setState({ failed: false });
192
+ }
193
+ }
194
+ render() {
195
+ if (this.state.failed) {
196
+ return null;
197
+ }
198
+ return this.props.children;
199
+ }
200
+ }
160
201
  function RSCRouter({ initialResponse, initialPathname }) {
161
202
  const [flightResponse, setFlightResponse] = React.useState(initialResponse);
162
203
  const [pathname, setPathname] = React.useState(initialPathname || (typeof window !== 'undefined' ? window.location.pathname : '/'));
@@ -164,6 +205,13 @@ function RSCRouter({ initialResponse, initialPathname }) {
164
205
  ? new URLSearchParams(window.location.search)
165
206
  : new URLSearchParams());
166
207
  const [isPending, startTransition] = React.useTransition();
208
+ const href = `${pathname}${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
209
+ React.useEffect(() => {
210
+ const initialKey = cacheKey(initialPathname || (typeof window !== 'undefined' ? window.location.pathname : '/'), typeof window !== 'undefined' ? window.location.search : '');
211
+ if (!flightCache.has(initialKey)) {
212
+ flightCache.set(initialKey, initialResponse);
213
+ }
214
+ }, [initialPathname, initialResponse]);
167
215
  // Handle browser back/forward
168
216
  React.useEffect(() => {
169
217
  const onPopState = () => {
@@ -214,6 +262,33 @@ function RSCRouter({ initialResponse, initialPathname }) {
214
262
  setFlightResponse(fetchFlight(curPath, curSearch));
215
263
  });
216
264
  }, []);
265
+ const resume = React.useCallback((url) => {
266
+ const parsed = new URL(url, window.location.origin);
267
+ const nextPath = parsed.pathname;
268
+ const nextSearch = parsed.search;
269
+ const nextUrl = `${nextPath}${nextSearch}`;
270
+ const nextResponse = fetchFlight(nextPath, nextSearch);
271
+ document.documentElement.setAttribute('data-vista-ppr', 'flight-resuming');
272
+ recordRuntimeTrace('rsc-resume-start', { url: nextUrl });
273
+ dispatchRuntimeEvent('vista:rsc-resume-start', { url: nextUrl });
274
+ startTransition(() => {
275
+ setPathname(nextPath);
276
+ setSearchParams(new URLSearchParams(nextSearch));
277
+ setFlightResponse(nextResponse);
278
+ });
279
+ Promise.resolve(nextResponse)
280
+ .then(() => {
281
+ recordRuntimeTrace('rsc-resume-complete', { url: nextUrl });
282
+ dispatchRuntimeEvent('vista:rsc-resume-complete', { url: nextUrl });
283
+ })
284
+ .catch((error) => {
285
+ const message = error && typeof error === 'object' && 'message' in error
286
+ ? String(error.message || error)
287
+ : String(error || 'Unknown RSC resume error');
288
+ recordRuntimeTrace('rsc-resume-error', { url: nextUrl, message });
289
+ dispatchRuntimeEvent('vista:rsc-resume-error', { url: nextUrl, message });
290
+ });
291
+ }, []);
217
292
  const contextValue = React.useMemo(() => ({
218
293
  pathname,
219
294
  searchParams,
@@ -225,59 +300,31 @@ function RSCRouter({ initialResponse, initialPathname }) {
225
300
  refresh,
226
301
  isPending,
227
302
  }), [pathname, searchParams, push, replace, back, forward, prefetch, refresh, isPending]);
228
- React.useEffect(() => {
229
- if (typeof window === 'undefined') {
230
- return;
231
- }
232
- const bridge = {
233
- refresh,
234
- prefetch,
235
- resume: (url) => {
236
- const parsed = new URL(url, window.location.origin);
237
- const nextPath = parsed.pathname;
238
- const nextSearch = parsed.search;
239
- const nextUrl = `${nextPath}${nextSearch}`;
240
- const nextResponse = fetchFlight(nextPath, nextSearch);
241
- document.documentElement.setAttribute('data-vista-ppr', 'flight-resuming');
242
- recordRuntimeTrace('rsc-resume-start', { url: nextUrl });
243
- dispatchRuntimeEvent('vista:rsc-resume-start', { url: nextUrl });
244
- startTransition(() => {
245
- setPathname(nextPath);
246
- setSearchParams(new URLSearchParams(nextSearch));
247
- setFlightResponse(nextResponse);
248
- });
249
- Promise.resolve(nextResponse)
250
- .then(() => {
251
- recordRuntimeTrace('rsc-resume-complete', { url: nextUrl });
252
- dispatchRuntimeEvent('vista:rsc-resume-complete', { url: nextUrl });
253
- })
254
- .catch((error) => {
255
- const message = error && typeof error === 'object' && 'message' in error
256
- ? String(error.message || error)
257
- : String(error || 'Unknown RSC resume error');
258
- recordRuntimeTrace('rsc-resume-error', { url: nextUrl, message });
259
- dispatchRuntimeEvent('vista:rsc-resume-error', { url: nextUrl, message });
260
- });
261
- },
303
+ if (typeof window !== 'undefined') {
304
+ window.__VISTA_RSC_ROUTER__ = {
305
+ ...contextValue,
306
+ resume,
262
307
  getState: () => ({
263
308
  pathname,
264
309
  search: searchParams.toString() ? `?${searchParams.toString()}` : '',
265
310
  isPending,
266
311
  }),
267
312
  };
268
- window.__VISTA_RSC_ROUTER__ = bridge;
313
+ }
314
+ React.useEffect(() => {
315
+ if (typeof window === 'undefined') {
316
+ return;
317
+ }
269
318
  recordRuntimeTrace('rsc-router-ready', {
270
319
  pathname,
271
320
  search: searchParams.toString() ? `?${searchParams.toString()}` : '',
272
321
  });
273
322
  document.dispatchEvent(new CustomEvent('vista:rsc-router-ready'));
274
323
  return () => {
275
- if (window.__VISTA_RSC_ROUTER__ === bridge) {
276
- delete window.__VISTA_RSC_ROUTER__;
277
- }
324
+ delete window.__VISTA_RSC_ROUTER__;
278
325
  };
279
- }, [pathname, searchParams, isPending, refresh, prefetch]);
280
- return ((0, jsx_runtime_1.jsx)(exports.RSCRouterContext.Provider, { value: contextValue, children: (0, jsx_runtime_1.jsx)(RSCRoot, { response: flightResponse }) }));
326
+ }, []);
327
+ return ((0, jsx_runtime_1.jsx)(exports.RSCRouterContext.Provider, { value: contextValue, children: (0, jsx_runtime_1.jsx)(FlightNavigationErrorBoundary, { href: href, children: (0, jsx_runtime_1.jsx)(RSCRoot, { response: flightResponse }) }) }));
281
328
  }
282
329
  // ---------------------------------------------------------------------------
283
330
  // Hooks — unified versions that work in both RSC and legacy modes
@@ -287,5 +334,11 @@ function RSCRouter({ initialResponse, initialPathname }) {
287
334
  * otherwise falls back to null.
288
335
  */
289
336
  function useRSCRouter() {
290
- return React.useContext(exports.RSCRouterContext);
337
+ const ctx = React.useContext(exports.RSCRouterContext);
338
+ if (ctx)
339
+ return ctx;
340
+ if (typeof window !== 'undefined' && typeof window.__VISTA_RSC_ROUTER__?.push === 'function') {
341
+ return window.__VISTA_RSC_ROUTER__;
342
+ }
343
+ return null;
291
344
  }
@@ -25,28 +25,6 @@ pages_build_output_dir = "${relativeOutput}"
25
25
  (0, utils_1.writeFileIfAllowed)(targetFile, content, ctx.force);
26
26
  return targetFile;
27
27
  }
28
- function writeRoutesJson(outputDir) {
29
- const routesPath = path_1.default.join(outputDir, '_routes.json');
30
- const routes = {
31
- version: 1,
32
- include: ['/*'],
33
- exclude: ['/static/*'],
34
- };
35
- fs_1.default.writeFileSync(routesPath, `${JSON.stringify(routes, null, 2)}\n`, 'utf8');
36
- return routesPath;
37
- }
38
- function writeRedirects(outputDir) {
39
- const redirectsPath = path_1.default.join(outputDir, '_redirects');
40
- const lines = [
41
- '/_vista/* /:splat 200',
42
- '/ /static/pages/index.html 200',
43
- '/rsc /static/pages/index.rsc 200',
44
- '/_rsc/* /static/pages/:splat.rsc 200',
45
- '/* /static/pages/:splat.html 200',
46
- ];
47
- fs_1.default.writeFileSync(redirectsPath, `${lines.join('\n')}\n`, 'utf8');
48
- return redirectsPath;
49
- }
50
28
  exports.cloudflareAdapter = {
51
29
  id: 'cloudflare',
52
30
  requiredOutput: 'standalone',
@@ -63,13 +41,12 @@ exports.cloudflareAdapter = {
63
41
  (0, utils_1.ensureDir)(outputDir);
64
42
  if ((0, runtime_pack_1.isStaticOnlyDeploy)(ctx)) {
65
43
  (0, utils_1.copyStaticHostAssets)(ctx.cwd, ctx.vistaDir, outputDir);
66
- const routesPath = writeRoutesJson(outputDir);
67
- const redirectsPath = writeRedirects(outputDir);
44
+ (0, utils_1.prepareStaticCdnOutput)(outputDir);
68
45
  const wranglerPath = writeStaticWranglerToml(ctx, outputDir);
69
46
  return {
70
47
  status: 'emitted',
71
48
  target: 'cloudflare',
72
- artifactPaths: [outputDir, routesPath, redirectsPath, wranglerPath],
49
+ artifactPaths: [outputDir, wranglerPath],
73
50
  instructions: [
74
51
  'Static mode: Cloudflare Pages serves pre-rendered output.',
75
52
  'For Flight SSR, omit deploy.output "static" and use Cloudflare Containers.',