vinext 1.4.1 → 1.4.2

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 (83) hide show
  1. package/README.md +1 -1
  2. package/dist/build/action-owner-manifest.js +1 -0
  3. package/dist/check.js +1 -1
  4. package/dist/config/config-matchers.js +2 -2
  5. package/dist/config/destination-params.js +5 -2
  6. package/dist/config/next-config.d.ts +16 -1
  7. package/dist/config/next-config.js +31 -1
  8. package/dist/entries/app-rsc-entry.js +41 -18
  9. package/dist/entries/app-rsc-manifest.js +4 -0
  10. package/dist/entries/pages-server-entry.js +2 -0
  11. package/dist/index.js +15 -12
  12. package/dist/init-cloudflare.js +18 -2
  13. package/dist/plugins/css-target.d.ts +18 -0
  14. package/dist/plugins/css-target.js +93 -0
  15. package/dist/routing/app-route-graph.d.ts +10 -0
  16. package/dist/routing/app-route-graph.js +15 -1
  17. package/dist/server/api-handler.d.ts +2 -0
  18. package/dist/server/api-handler.js +3 -5
  19. package/dist/server/app-action-forwarding.js +3 -1
  20. package/dist/server/app-browser-entry.js +21 -4
  21. package/dist/server/app-elements.d.ts +3 -0
  22. package/dist/server/app-elements.js +14 -1
  23. package/dist/server/app-fallback-renderer.js +2 -2
  24. package/dist/server/app-optimistic-routing.d.ts +12 -1
  25. package/dist/server/app-optimistic-routing.js +38 -16
  26. package/dist/server/app-page-boundary-render.js +2 -2
  27. package/dist/server/app-page-cache-render.js +1 -1
  28. package/dist/server/app-page-dispatch.d.ts +10 -1
  29. package/dist/server/app-page-dispatch.js +73 -30
  30. package/dist/server/app-page-element-builder.d.ts +9 -20
  31. package/dist/server/app-page-element-builder.js +37 -49
  32. package/dist/server/app-page-intercept-segment-config.d.ts +56 -0
  33. package/dist/server/app-page-intercept-segment-config.js +90 -0
  34. package/dist/server/app-page-navigation-params.d.ts +23 -0
  35. package/dist/server/app-page-navigation-params.js +49 -0
  36. package/dist/server/app-page-render.d.ts +5 -2
  37. package/dist/server/app-page-render.js +17 -3
  38. package/dist/server/app-page-request.d.ts +24 -2
  39. package/dist/server/app-page-request.js +10 -10
  40. package/dist/server/app-page-route-wiring.d.ts +10 -0
  41. package/dist/server/app-page-route-wiring.js +16 -12
  42. package/dist/server/app-page-segment-plan.js +9 -1
  43. package/dist/server/app-page-segment-state.d.ts +9 -0
  44. package/dist/server/app-page-segment-state.js +54 -1
  45. package/dist/server/app-response-stage-entry.js +1 -0
  46. package/dist/server/app-route-module-loader.d.ts +2 -0
  47. package/dist/server/app-route-module-loader.js +1 -0
  48. package/dist/server/app-rsc-handler.d.ts +10 -1
  49. package/dist/server/app-rsc-handler.js +75 -43
  50. package/dist/server/app-rsc-response-finalizer.d.ts +3 -0
  51. package/dist/server/app-rsc-response-finalizer.js +11 -1
  52. package/dist/server/app-rsc-route-matching.d.ts +14 -0
  53. package/dist/server/app-rsc-route-matching.js +42 -18
  54. package/dist/server/app-server-action-execution.js +16 -10
  55. package/dist/server/cache-control.d.ts +2 -0
  56. package/dist/server/cache-control.js +3 -1
  57. package/dist/server/image-local-patterns.d.ts +21 -0
  58. package/dist/server/image-local-patterns.js +59 -0
  59. package/dist/server/image-optimization.d.ts +23 -22
  60. package/dist/server/image-optimization.js +12 -2
  61. package/dist/server/metadata-route-response.js +24 -6
  62. package/dist/server/nitro-public-files-plugin.js +20 -8
  63. package/dist/server/pages-data-route.d.ts +5 -1
  64. package/dist/server/pages-data-route.js +14 -6
  65. package/dist/server/pages-request-pipeline.js +6 -5
  66. package/dist/server/pages-request-stage-entry.js +1 -0
  67. package/dist/server/prod-server.js +17 -9
  68. package/dist/server/request-pipeline.d.ts +5 -0
  69. package/dist/server/request-pipeline.js +11 -1
  70. package/dist/server/response-stage-cacheability.d.ts +7 -0
  71. package/dist/server/response-stage-cacheability.js +6 -5
  72. package/dist/server/worker-image-optimization.d.ts +2 -2
  73. package/dist/server/worker-image-optimization.js +18 -3
  74. package/dist/server/worker-utils.d.ts +8 -0
  75. package/dist/server/worker-utils.js +19 -2
  76. package/dist/shims/image-props.d.ts +6 -0
  77. package/dist/shims/image-props.js +39 -3
  78. package/dist/shims/image.js +3 -3
  79. package/dist/shims/slot.d.ts +3 -0
  80. package/dist/shims/slot.js +67 -9
  81. package/dist/utils/base-path.d.ts +7 -0
  82. package/dist/utils/base-path.js +11 -1
  83. package/package.json +1 -1
package/README.md CHANGED
@@ -687,7 +687,7 @@ Every `next/*` import is shimmed to a Vite-compatible implementation.
687
687
  | `next.config.js` / `.ts` / `.mjs` | ✅ | Function configs, phase argument |
688
688
  | `rewrites` / `redirects` / `headers` | ✅ | All phases, param interpolation. A source Next.js rejects fails config loading, as it fails the Next.js build: missing, not starting with `/`, over the 4,096-character built-regex limit, or rejected by path-to-regexp, such as a header glob `/api/*` |
689
689
  | Environment variables (`.env*`, `NEXT_PUBLIC_*`) | ✅ | Auto-loads Next.js-style dotenv files; only public vars are inlined |
690
- | `images` config | 🟡 | Parsed but not used for optimization |
690
+ | `images` config | 🟡 | Parsed but not used for optimization; the production image endpoint enforces `localPatterns` (dev serves any local source) |
691
691
  | `experimental.optimizePackageImports` | ✅ | Rewrites barrel imports to direct sub-module imports in RSC/SSR environments. A default set (lucide-react, date-fns, radix-ui, antd, MUI, and others) are always optimized. Add package names here to extend the list. |
692
692
  | `vinext({ nextConfig })` | ✅ | Inline Next-style config from `vite.config.*`. Supports object-form and function-form config. When provided, this overrides root `next.config.*`. |
693
693
  | `vinext({ react: { compiler: true } })` | ✅ | React Compiler auto memoization. Needs `@vitejs/plugin-react` 6.1+ and the optional `oxc-transform-react` package. |
@@ -26,6 +26,7 @@ function actionOwnerRouteEntryIds(route) {
26
26
  slot.loadingPath,
27
27
  ...slot.loadingPaths ?? [],
28
28
  slot.errorPath,
29
+ ...slot.errorPaths ?? [],
29
30
  slot.notFoundPath,
30
31
  ...slot.notFoundPaths ?? [],
31
32
  ...slot.interceptingRoutes.flatMap((intercept) => [
package/dist/check.js CHANGED
@@ -197,7 +197,7 @@ const CONFIG_SUPPORT = {
197
197
  env: { status: "supported" },
198
198
  images: {
199
199
  status: "partial",
200
- detail: "remotePatterns, deviceSizes/imageSizes, qualities and SVG/CSP options honoured; resizing needs an optimizer via vinext({ images: { optimizer } }), images are served as-is otherwise; loader/loaderFile are ignored"
200
+ detail: "remotePatterns, deviceSizes/imageSizes, qualities, localPatterns (production only) and SVG/CSP options honoured; resizing needs an optimizer via vinext({ images: { optimizer } }), images are served as-is otherwise; loader/loaderFile are ignored"
201
201
  },
202
202
  allowedDevOrigins: {
203
203
  status: "supported",
@@ -719,7 +719,7 @@ function preserveRedirectDestinationQuery(destination, requestSearch, destinatio
719
719
  }
720
720
  const LOCATION_STRIPPED_RE = /^[\u0000- ]+|[\u0000- ]+$|[\t\n\r]/g;
721
721
  const LOCATION_UNSAFE_RE = /[\u0000-\u0008\u000A-\u001F\u007F]|[^\u0000-\u007F]/gu;
722
- const LONE_SURROGATE_RE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
722
+ const SURROGATE_RE = /[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDFFF]/g;
723
723
  /**
724
724
  * Percent-encode what a config-redirect `Location` header cannot carry.
725
725
  *
@@ -731,7 +731,7 @@ const LONE_SURROGATE_RE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF
731
731
  * constructor throws the same way); vinext percent-encodes them instead.
732
732
  */
733
733
  function encodeRedirectLocation(location) {
734
- return location.replace(LONE_SURROGATE_RE, "�").replace(LOCATION_UNSAFE_RE, encodeURIComponent);
734
+ return location.replace(SURROGATE_RE, (surrogate) => surrogate.length === 2 ? surrogate : "�").replace(LOCATION_UNSAFE_RE, encodeURIComponent);
735
735
  }
736
736
  function parseExternalRewriteUrl(externalUrl) {
737
737
  try {
@@ -91,9 +91,12 @@ function substituteRewritePath(template, paramRe, params, absolute) {
91
91
  const [path, hash] = substituted.split("#", 2);
92
92
  const pathHash = hash === void 0 ? void 0 : hash ? `#${hash}` : "";
93
93
  if (!absolute) return [path, pathHash];
94
- return [path.replace(DOT_SEGMENT_RE, (segment, offset) => paramRanges.some(([start, end]) => start <= offset + segment.length && end >= offset) ? segment.replace(/\.|%(?=2e)/gi, (char) => char === "." ? "%252E" : "%25") : segment), pathHash];
94
+ return [path.replace(DOT_SEGMENT_RE, (_match, separator, segment, offset) => {
95
+ const start = offset + separator.length;
96
+ return paramRanges.some(([paramStart, end]) => paramStart <= start + segment.length && end >= start) ? separator + segment.replace(/\.|%(?=2e)/gi, (char) => char === "." ? "%252E" : "%25") : separator + segment;
97
+ }), pathHash];
95
98
  }
96
- const DOT_SEGMENT_RE = /(?<=^|[/\\])(?:\.|%2e){1,2}(?=[/\\]|$)/gi;
99
+ const DOT_SEGMENT_RE = /(^|[/\\])((?:\.|%2e){1,2})(?=[/\\]|$)/gi;
97
100
  function encodePathQueryDelimiter(value) {
98
101
  return value.replaceAll("?", "%3F");
99
102
  }
@@ -183,12 +183,27 @@ export type NextConfig = {
183
183
  search?: string;
184
184
  }>;
185
185
  domains?: string[];
186
+ /**
187
+ * Local sources the image optimization endpoint may serve. Statically
188
+ * imported images are allowed too, except under a basePath (the added
189
+ * pattern is unprefixed, as in Next.js). When unset, any local path
190
+ * without a query string is allowed (matches Next.js).
191
+ */
192
+ localPatterns?: Array<{
193
+ pathname?: string;
194
+ search?: string;
195
+ }>;
186
196
  unoptimized?: boolean;
187
197
  /** Allowed device widths for image optimization. Defaults to Next.js defaults: [640, 750, 828, 1080, 1200, 1920, 2048, 3840] */
188
198
  deviceSizes?: number[];
189
199
  /** Allowed image sizes for fixed-width images. Defaults to Next.js defaults: [32, 48, 64, 96, 128, 256, 384] */
190
200
  imageSizes?: number[];
191
- /** Allowed image qualities. When unset, any quality from 1-100 is permitted (matches Next.js). */
201
+ /**
202
+ * Allowed image qualities. When set, the endpoint accepts only these and the
203
+ * default loader rounds `quality` to the closest entry, as in Next.js. When
204
+ * unset, any quality from 1-100 is permitted: a deliberate divergence from
205
+ * Next.js 16's `[75]` default that keeps Next.js 14/15 apps working.
206
+ */
192
207
  qualities?: number[];
193
208
  /** Allow SVG images through the image optimization endpoint. SVG can contain scripts, so only enable if you trust all image sources. */
194
209
  dangerouslyAllowSVG?: boolean;
@@ -507,6 +507,35 @@ async function resolveBuildId(generate) {
507
507
  return trimmed;
508
508
  }
509
509
  /**
510
+ * Mirror Next.js's config resolution of `images.localPatterns`: statically
511
+ * imported images (`/_next/static/media/**`) stay allowed whatever the app
512
+ * lists, though like Next.js the pattern is unprefixed, so it never matches
513
+ * under a basePath. An unset list stays unset; the image endpoint then applies the
514
+ * Next.js default.
515
+ */
516
+ function resolveImageLocalPatterns(localPatterns) {
517
+ if (localPatterns === void 0) return void 0;
518
+ if (!Array.isArray(localPatterns)) throw new Error(`Specified images.localPatterns should be an Array received ${localPatterns === null ? "null" : typeof localPatterns}.\nSee more info here: https://nextjs.org/docs/messages/invalid-images-config`);
519
+ if (localPatterns.length > 25) throw new Error("Specified images.localPatterns can have at most 25 entries.");
520
+ const patterns = Array.from(localPatterns, (pattern, index) => {
521
+ const entry = pattern;
522
+ let valid = typeof pattern === "object" && pattern !== null && !Array.isArray(pattern) && !(pattern instanceof Date) && !(pattern instanceof Map) && !(pattern instanceof Set) && !(typeof entry.then === "function" && typeof pattern.catch === "function") && (entry.pathname === void 0 || typeof entry.pathname === "string") && (entry.search === void 0 || typeof entry.search === "string");
523
+ if (valid) {
524
+ for (const key in pattern) if (key !== "pathname" && key !== "search") valid = false;
525
+ }
526
+ if (!valid) throw new Error(`Specified images.localPatterns[${index}] should be an object with optional string \`pathname\` and \`search\` fields.`);
527
+ const resolved = {};
528
+ if (entry.pathname !== void 0) resolved.pathname = entry.pathname;
529
+ if (entry.search !== void 0) resolved.search = entry.search;
530
+ return resolved;
531
+ });
532
+ if (!patterns.some((pattern) => pattern.pathname === "/_next/static/media/**" && pattern.search === "")) patterns.push({
533
+ pathname: "/_next/static/media/**",
534
+ search: ""
535
+ });
536
+ return patterns;
537
+ }
538
+ /**
510
539
  * Normalize the `assetPrefix` option from next.config.
511
540
  *
512
541
  * Accepts both absolute URLs (`https://cdn.example.com[/subpath]`) and
@@ -1094,7 +1123,8 @@ async function resolveNextConfig(config, root = toSlash(process.cwd()), options
1094
1123
  port: pattern.port,
1095
1124
  pathname: pattern.pathname,
1096
1125
  search: pattern.search
1097
- } : { ...pattern })
1126
+ } : { ...pattern }),
1127
+ localPatterns: resolveImageLocalPatterns(config.images.localPatterns)
1098
1128
  } : void 0;
1099
1129
  const resolved = {
1100
1130
  env: config.env ?? {},
@@ -94,9 +94,10 @@ function appPageSegmentConfigPaths(route) {
94
94
  ];
95
95
  }
96
96
  /**
97
- * Patterns of the static App pages that answer a request whose raw pathname
98
- * reaches a dynamic sibling but whose segment-decoded pathname names them, so
99
- * `/%61` and `/caf%C3%A9` render `/a` and `/café` rather than `/[slug]`.
97
+ * Patterns of the static App pages that may answer a request whose raw
98
+ * pathname reaches a dynamic sibling but whose segment-decoded pathname names
99
+ * them, so `/%61` and `/caf%C3%A9` can render `/a` and `/café` rather than
100
+ * `/[slug]`.
100
101
  *
101
102
  * Next.js serves those requests from the response cached under the decoded
102
103
  * pathname, which exists only for a page it prerendered. The prerender listing
@@ -105,7 +106,10 @@ function appPageSegmentConfigPaths(route) {
105
106
  * runtime on any segment) and a Route Handler have no such entry, so the
106
107
  * dynamic sibling renders. So does a page whose sources can't be read here;
107
108
  * its sibling's render stays out of the shared caches. A cacheComponents
108
- * build lists every page route, as the prerender listing does.
109
+ * build lists every page route, as the prerender listing does. Segment config
110
+ * can't tell a page that reads request data, so the matcher renders the page
111
+ * for another spelling (`/%61dmin`) only once the build recorded the decoded
112
+ * pathname's prerender; the browser's own spelling needs none.
109
113
  */
110
114
  function buildAppDecodedPathnamePatterns(routes, config) {
111
115
  const isPrerenderedPage = (route) => {
@@ -148,6 +152,10 @@ function buildAppRequestRouteMetadata(routes) {
148
152
  slots: Object.fromEntries(route.parallelSlots.map((slot) => [slot.key, {
149
153
  id: slot.id ?? null,
150
154
  name: slot.name,
155
+ __loadPage: slot.pagePath ? true : null,
156
+ __loadDefault: slot.defaultPath ? true : null,
157
+ slotPatternParts: slot.slotPatternParts ?? null,
158
+ slotParamNames: slot.slotParamNames ?? null,
151
159
  intercepts: slot.interceptingRoutes.map((intercept) => ({
152
160
  id: intercept.id ?? null,
153
161
  targetPattern: intercept.targetPattern,
@@ -244,6 +252,8 @@ import { dispatchAppRequestStage as __dispatchAppRequestStage } from ${JSON.stri
244
252
  import { registerConfiguredCacheAdapters as __registerConfiguredCacheAdapters } from "virtual:vinext-cdn-cache-adapter";
245
253
  import { clearAppRequestStageContext as __clearRequestContext, setAppRequestStageNavigationContext as setNavigationContext } from ${JSON.stringify(appRequestStageContextPath)};
246
254
  import { matchRoutePattern as __matchRoutePattern } from ${JSON.stringify(routePatternPath)};
255
+ import { initPregeneratedPathsFromGlobals as __initPregeneratedPathsFromGlobals } from ${JSON.stringify(pregeneratedConcretePathsPath)};
256
+ import "virtual:vinext-pregenerated-concrete-paths";
247
257
  import __pagesClientAssets from "virtual:vinext-pages-client-assets";
248
258
  ${middlewarePath ? `import { applyAppMiddleware as __applyAppMiddleware } from ${JSON.stringify(appMiddlewarePath)};` : ""}
249
259
  ${instrumentationPath ? `import * as _instrumentation from ${JSON.stringify(toSlash(instrumentationPath))};
@@ -278,12 +288,16 @@ export { __basePath };
278
288
  export const __imageAllowedWidths = ${JSON.stringify([...config?.imageConfig?.deviceSizes ?? DEFAULT_DEVICE_SIZES, ...config?.imageConfig?.imageSizes ?? DEFAULT_IMAGE_SIZES])};
279
289
  export const __imageConfig = ${JSON.stringify({
280
290
  qualities: config?.imageConfig?.qualities,
291
+ localPatterns: config?.imageConfig?.localPatterns,
281
292
  dangerouslyAllowSVG: config?.imageConfig?.dangerouslyAllowSVG,
282
293
  dangerouslyAllowLocalIP: config?.imageConfig?.dangerouslyAllowLocalIP,
283
294
  contentDispositionType: config?.imageConfig?.contentDispositionType,
284
295
  contentSecurityPolicy: config?.imageConfig?.contentSecurityPolicy
285
296
  })};
286
297
  export const __clientBuildFiles = __pagesClientAssets.clientBuildFiles ?? [];
298
+ // The matcher reads recorded prerenders, and this stage can run without the
299
+ // response entry that also loads them.
300
+ __initPregeneratedPathsFromGlobals();
287
301
  const __routes = ${JSON.stringify(requestRoutes)};
288
302
  const __routeMatcher = __createAppRscRouteMatcher(__routes, ${JSON.stringify(buildAppDecodedPathnamePatterns(routes, config))});
289
303
  const __metadataRouteMatchers = ${JSON.stringify(metadataRouteMatchers)};
@@ -354,17 +368,21 @@ const __requestHandler = createAppRscRequestHandler({
354
368
  const intercept = __routeMatcher.findIntercept(pathname, sourcePathname, interceptionId);
355
369
  if (!intercept) return null;
356
370
  const route = __routes[intercept.sourceRouteIndex];
357
- if (!route) return null;
358
- const params = Object.create(null);
359
- for (const name of route.params) {
360
- if (Object.prototype.hasOwnProperty.call(intercept.sourceMatchedParams, name)) {
361
- params[name] = intercept.sourceMatchedParams[name];
371
+ const authorizationRoute = __routes[intercept.authorizationSourceRouteIndex];
372
+ if (!route || !authorizationRoute) return null;
373
+ const pickParams = (targetRoute, source) => {
374
+ const params = Object.create(null);
375
+ for (const name of targetRoute.params) {
376
+ if (Object.prototype.hasOwnProperty.call(source, name)) params[name] = source[name];
362
377
  }
363
- }
378
+ return params;
379
+ };
364
380
  return {
381
+ authorizationParams: pickParams(authorizationRoute, intercept.authorizationSourceParams),
382
+ authorizationRoute,
365
383
  interceptionSourceIsConcrete: intercept.sourceRouteIsConcrete,
366
384
  route,
367
- params,
385
+ params: pickParams(route, intercept.sourceMatchedParams),
368
386
  };
369
387
  },
370
388
  ${middlewarePath ? `runMiddleware({ cleanPathname, context, externalRewriteRequest, hadBasePath, isDataRequest, middlewareRequest, request, validateExternalRewriteRequest }) {
@@ -482,6 +500,7 @@ function generateRscEntry(appDir, routes, middlewarePath, metadataRoutes, global
482
500
  const imageAllowedWidths = [...config?.imageConfig?.deviceSizes ?? DEFAULT_DEVICE_SIZES, ...config?.imageConfig?.imageSizes ?? DEFAULT_IMAGE_SIZES];
483
501
  const imageConfig = {
484
502
  qualities: config?.imageConfig?.qualities,
503
+ localPatterns: config?.imageConfig?.localPatterns,
485
504
  dangerouslyAllowSVG: config?.imageConfig?.dangerouslyAllowSVG,
486
505
  dangerouslyAllowLocalIP: config?.imageConfig?.dangerouslyAllowLocalIP,
487
506
  contentDispositionType: config?.imageConfig?.contentDispositionType,
@@ -1635,17 +1654,21 @@ ${responseStageOnly ? "const __responseStageOptions = {" : "const __appRscHandle
1635
1654
  const intercept = findIntercept(pathname, sourcePathname, interceptionId);
1636
1655
  if (!intercept) return null;
1637
1656
  const route = routes[intercept.sourceRouteIndex];
1638
- if (!route) return null;
1639
- const params = Object.create(null);
1640
- for (const name of route.params) {
1641
- if (Object.prototype.hasOwnProperty.call(intercept.sourceMatchedParams, name)) {
1642
- params[name] = intercept.sourceMatchedParams[name];
1657
+ const authorizationRoute = routes[intercept.authorizationSourceRouteIndex];
1658
+ if (!route || !authorizationRoute) return null;
1659
+ const pickParams = (targetRoute, source) => {
1660
+ const params = Object.create(null);
1661
+ for (const name of targetRoute.params) {
1662
+ if (Object.prototype.hasOwnProperty.call(source, name)) params[name] = source[name];
1643
1663
  }
1644
- }
1664
+ return params;
1665
+ };
1645
1666
  return {
1667
+ authorizationParams: pickParams(authorizationRoute, intercept.authorizationSourceParams),
1668
+ authorizationRoute,
1646
1669
  interceptionSourceIsConcrete: intercept.sourceRouteIsConcrete,
1647
1670
  route,
1648
- params,
1671
+ params: pickParams(route, intercept.sourceMatchedParams),
1649
1672
  };
1650
1673
  },
1651
1674
  ${middlewarePath ? `runMiddleware({ cleanPathname, context, externalRewriteRequest, hadBasePath, isDataRequest, middlewareRequest, request, validateExternalRewriteRequest }) {
@@ -108,6 +108,7 @@ function registerRouteModules(routes, imports) {
108
108
  if (slot.loadingPath) imports.getLazyLoaderVar(slot.loadingPath);
109
109
  for (const loadingPath of slot.loadingPaths ?? []) imports.getLazyLoaderVar(loadingPath);
110
110
  if (slot.errorPath) imports.getLazyLoaderVar(slot.errorPath);
111
+ for (const errorPath of slot.errorPaths ?? []) imports.getLazyLoaderVar(errorPath);
111
112
  if (slot.notFoundPath) imports.getLazyLoaderVar(slot.notFoundPath);
112
113
  for (const notFoundPath of slot.notFoundPaths ?? []) imports.getLazyLoaderVar(notFoundPath);
113
114
  for (const ir of slot.interceptingRoutes) {
@@ -240,6 +241,9 @@ function buildRouteEntries(routes, imports) {
240
241
  loadingTreePositions: ${JSON.stringify(slot.loadingTreePositions ?? [])},
241
242
  error: null,
242
243
  __loadError: ${slot.errorPath ? imports.getLazyLoaderVar(slot.errorPath) : "null"},
244
+ errors: ${moduleArray(slot.errorPaths?.length ?? 0)},
245
+ __loadErrors: ${lazyLoaderArray(slot.errorPaths ?? [], imports)},
246
+ errorTreePositions: ${JSON.stringify(slot.errorTreePositions ?? [])},
243
247
  notFound: null,
244
248
  __loadNotFound: ${slot.notFoundPath ? imports.getLazyLoaderVar(slot.notFoundPath) : "null"},
245
249
  notFoundTreePosition: ${slot.notFoundTreePosition ?? "null"},
@@ -104,6 +104,7 @@ async function generatePagesRequestEntry(pagesDir, nextConfig, fileMatcher, midd
104
104
  deviceSizes: nextConfig?.images?.deviceSizes,
105
105
  imageSizes: nextConfig?.images?.imageSizes,
106
106
  qualities: nextConfig?.images?.qualities,
107
+ localPatterns: nextConfig?.images?.localPatterns,
107
108
  dangerouslyAllowSVG: nextConfig?.images?.dangerouslyAllowSVG,
108
109
  dangerouslyAllowLocalIP: nextConfig?.images?.dangerouslyAllowLocalIP,
109
110
  contentDispositionType: nextConfig?.images?.contentDispositionType,
@@ -281,6 +282,7 @@ async function generateServerEntry(pagesDir, nextConfig, fileMatcher, middleware
281
282
  deviceSizes: nextConfig?.images?.deviceSizes,
282
283
  imageSizes: nextConfig?.images?.imageSizes,
283
284
  qualities: nextConfig?.images?.qualities,
285
+ localPatterns: nextConfig?.images?.localPatterns,
284
286
  dangerouslyAllowSVG: nextConfig?.images?.dangerouslyAllowSVG,
285
287
  dangerouslyAllowLocalIP: nextConfig?.images?.dangerouslyAllowLocalIP,
286
288
  contentDispositionType: nextConfig?.images?.contentDispositionType,
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ import { PHASE_DEVELOPMENT_SERVER, PHASE_PRODUCTION_BUILD } from "./shims/consta
14
14
  import { serializeCacheLifeProfiles } from "./utils/cache-life-profiles.js";
15
15
  import { VINEXT_MW_CTX_HEADER } from "./utils/protocol-headers.js";
16
16
  import { encodeMiddlewareRequestHeaders } from "./utils/middleware-request-headers.js";
17
- import { hasBasePath, stripBasePath } from "./utils/base-path.js";
17
+ import { hasBasePath, restoreBasePath, stripBasePath } from "./utils/base-path.js";
18
18
  import { INTERNAL_HEADERS, NEXTJS_DEPLOYMENT_ID_HEADER, VINEXT_INTERNAL_HEADERS, VINEXT_TIMING_HEADER } from "./server/headers.js";
19
19
  import { methodNotAllowedResponse, notFoundStaticAssetResponse } from "./server/http-error-responses.js";
20
20
  import { isOpenRedirectShaped, sendRepeatedSlashRedirect } from "./server/open-redirect.js";
@@ -89,6 +89,7 @@ import { createClientCompileHintsPlugin } from "./plugins/client-compile-hints.j
89
89
  import { clientReferenceDedupPlugin } from "./plugins/client-reference-dedup.js";
90
90
  import { dataUrlCssPlugin } from "./plugins/css-data-url.js";
91
91
  import { createCssModuleImportCompatibilityPlugin } from "./plugins/css-module-imports.js";
92
+ import { createCssTargetPlugin } from "./plugins/css-target.js";
92
93
  import { createWorkerImageImportsPlugin } from "./plugins/worker-image-imports.js";
93
94
  import { assertClientOutDirIsSeparate, createClientAssetFileNames, createClientCodeSplittingConfig, createClientManualChunks, createClientOutputFileNameDefaults, createMultiStageChunkFileNames, createMultiStageCodeSplittingConfig, createRscFrameworkChunkOutputConfig, createWorkerFileNames, findUnsupportedClientOutputFileNames, getBuildBundlerOptions, getClientTreeshakeConfig, getOutputFileNames, withBuildBundlerOptions } from "./build/client-build-config.js";
94
95
  import { createRouteClientReferencesFallbackPlugin, createRscClientReferenceLoadersPlugin } from "./plugins/rsc-client-reference-loaders.js";
@@ -1466,6 +1467,7 @@ function vinext(options = {}) {
1466
1467
  }),
1467
1468
  dataUrlCssPlugin(),
1468
1469
  createCssModuleImportCompatibilityPlugin(),
1470
+ createCssTargetPlugin(),
1469
1471
  createDevServerLifecyclePlugin({}, (config) => config[VINEXT_DEV_CLI_LIFECYCLE] === true),
1470
1472
  {
1471
1473
  name: "vinext:config",
@@ -1764,12 +1766,6 @@ function vinext(options = {}) {
1764
1766
  } } : {},
1765
1767
  build: {
1766
1768
  ssrEmitAssets: true,
1767
- cssTarget: [
1768
- "chrome111",
1769
- "edge111",
1770
- "firefox114",
1771
- "safari15"
1772
- ],
1773
1769
  assetsDir: clientAssetsDir,
1774
1770
  ...!isSSR && !hasClientBuildEnvironment ? { assetsInlineLimit: clientAssetsInlineLimit } : {},
1775
1771
  ...withBuildBundlerOptions({
@@ -2453,6 +2449,7 @@ function vinext(options = {}) {
2453
2449
  deviceSizes: nextConfig?.images?.deviceSizes,
2454
2450
  imageSizes: nextConfig?.images?.imageSizes,
2455
2451
  qualities: nextConfig?.images?.qualities,
2452
+ localPatterns: nextConfig?.images?.localPatterns,
2456
2453
  dangerouslyAllowSVG: nextConfig?.images?.dangerouslyAllowSVG,
2457
2454
  dangerouslyAllowLocalIP: nextConfig?.images?.dangerouslyAllowLocalIP,
2458
2455
  contentDispositionType: nextConfig?.images?.contentDispositionType,
@@ -3378,14 +3375,17 @@ export const loadServerActionClient = ${hasServerActions ? `() => import(${JSON.
3378
3375
  const viteBase = server.config.base;
3379
3376
  const viteBasePath = viteBase.startsWith("/") && viteBase !== "/" ? viteBase.replace(/\/+$/, "") : "";
3380
3377
  const routingBasePath = bp || viteBasePath;
3378
+ const viteStrippedBasePath = originalRequestUrl !== originalEncodedUrl;
3379
+ let originalUrlHadBasePath = false;
3381
3380
  if (routingBasePath) {
3382
- if (hasBasePath(pathname, routingBasePath)) {
3381
+ if (!viteStrippedBasePath && hasBasePath(pathname, routingBasePath)) {
3383
3382
  const stripped = stripBasePath(pathname, routingBasePath);
3384
3383
  url = stripped + (url.includes("?") ? url.slice(url.indexOf("?")) : "");
3385
3384
  pathname = stripped;
3386
3385
  }
3387
3386
  const middlewarePathname = middlewareUrl.split("?")[0];
3388
3387
  if (hasBasePath(middlewarePathname, routingBasePath)) {
3388
+ originalUrlHadBasePath = true;
3389
3389
  const middlewareQs = middlewareUrl.includes("?") ? middlewareUrl.slice(middlewareUrl.indexOf("?")) : "";
3390
3390
  middlewareUrl = stripBasePath(middlewarePathname, routingBasePath) + middlewareQs;
3391
3391
  }
@@ -3395,7 +3395,7 @@ export const loadServerActionClient = ${hasServerActions ? `() => import(${JSON.
3395
3395
  routeUrl = stripBasePath(routePathname, routingBasePath) + routeQs;
3396
3396
  }
3397
3397
  }
3398
- let configMatchPathname = stripBasePath(middlewareUrl.split("?")[0], routingBasePath);
3398
+ let configMatchPathname = middlewareUrl.split("?")[0];
3399
3399
  if (nextConfig && !nextConfig.skipTrailingSlashRedirect) {
3400
3400
  const qs = url.includes("?") ? url.slice(url.indexOf("?")) : "";
3401
3401
  const trailingSlashRedirect = normalizeTrailingSlash(routeUrl.split("?")[0], bp, nextConfig.trailingSlash, qs);
@@ -3412,7 +3412,7 @@ export const loadServerActionClient = ${hasServerActions ? `() => import(${JSON.
3412
3412
  const rawRoutePathname = routeUrl.split("?")[0];
3413
3413
  if (isNextDataPathname(rawRoutePathname)) {
3414
3414
  const rawDataMatch = parseNextDataPathname(rawRoutePathname, devBuildId);
3415
- const dataMatch = rawDataMatch ? parseNextDataPathname(pathname, devBuildId) : null;
3415
+ const dataMatch = rawDataMatch ? parseNextDataPathname(pathname, devBuildId, true) : null;
3416
3416
  if (dataMatch && rawDataMatch) {
3417
3417
  isDataReq = true;
3418
3418
  const qs = url.includes("?") ? url.slice(url.indexOf("?")) : "";
@@ -3482,7 +3482,9 @@ export const loadServerActionClient = ${hasServerActions ? `() => import(${JSON.
3482
3482
  const devRunMiddlewareAdapter = capturedMiddlewarePath ? async (_request, _ctx, opts) => {
3483
3483
  const rawProto = process.env.VINEXT_TRUST_PROXY === "1" || (process.env.VINEXT_TRUSTED_HOSTS ?? "").split(",").some((h) => h.trim()) ? String(req.headers["x-forwarded-proto"] || "").split(",")[0].trim() : "";
3484
3484
  const mwOrigin = `${rawProto === "https" || rawProto === "http" ? rawProto : "http"}://${requestHost}`;
3485
- const middlewareRequest = new Request(new URL(middlewareUrl, mwOrigin), {
3485
+ const middlewareRequestUrl = new URL(middlewareUrl, mwOrigin);
3486
+ if (originalUrlHadBasePath) middlewareRequestUrl.pathname = restoreBasePath(middlewareRequestUrl.pathname, bp);
3487
+ const middlewareRequest = new Request(middlewareRequestUrl, {
3486
3488
  method: req.method,
3487
3489
  headers: nodeRequestHeaders,
3488
3490
  signal: webRequest.signal
@@ -3628,7 +3630,8 @@ export const loadServerActionClient = ${hasServerActions ? `() => import(${JSON.
3628
3630
  trustedRevalidateOrigin: getTrustedDevRevalidateOrigin(),
3629
3631
  allowedRevalidateHeaderKeys: nextConfig?.allowedRevalidateHeaderKeys,
3630
3632
  trailingSlash: nextConfig?.trailingSlash,
3631
- skipTrailingSlashRedirect: nextConfig?.skipTrailingSlashRedirect
3633
+ skipTrailingSlashRedirect: nextConfig?.skipTrailingSlashRedirect,
3634
+ requestPathname: originalUrlHadBasePath ? restoreBasePath(routeUrl.split("?")[0], bp) : routeUrl.split("?")[0]
3632
3635
  })) return;
3633
3636
  if (hasAppDir) {
3634
3637
  if (pipelineResult.configRewriteFired) forwardInternalApiRewriteToApp(pipelineResult.apiUrl);
@@ -140,7 +140,7 @@ function setupCloudflarePlatform(context, cloudflare) {
140
140
  generatedPlatformFiles.push(path.relative(context.root, responseStorePath) || RESPONSE_STORE_WRANGLER_CONFIG);
141
141
  }
142
142
  }
143
- const nextSteps = [];
143
+ const nextSteps = !wranglerCode || !findTopLevelJsonProperty(wranglerCode, "main") ? workerEntryWarning(context.root, context.isStaticExport) : [];
144
144
  if (cloudflare.cdnCache === "response-store" && (cloudflare.responseStoreMode ?? "service-binding") === "service-binding") nextSteps.push("Deploy Workers Response Store before deploying the application:", ` ${context.packageManager ?? "npm"} run deploy:response-store`);
145
145
  if (cloudflare.prerender && cloudflare.cdnCache !== "static-assets") nextSteps.push("Pre-rendered routes are built, but Cloudflare deploys do not serve them.", " Use the Static Assets cache to serve them, or cache warming to fill another cache.");
146
146
  return {
@@ -193,14 +193,15 @@ function setupCfPlatform(context, cloudflare) {
193
193
  }
194
194
  const configPath = path.join(context.root, "cloudflare.config.ts");
195
195
  const generatedPlatformFiles = [];
196
+ const nextSteps = [];
196
197
  if (!fs.existsSync(configPath)) {
197
198
  fs.writeFileSync(configPath, generateTypedCloudflareConfig({
198
199
  ...projectInfo,
199
200
  isStaticExport: context.isStaticExport
200
201
  }, cloudflare, context.today));
201
202
  generatedPlatformFiles.push("cloudflare.config.ts");
203
+ nextSteps.push(...workerEntryWarning(context.root, context.isStaticExport));
202
204
  }
203
- const nextSteps = [];
204
205
  if (serviceBinding) nextSteps.push("After `vinext build`, deploy the Response Store Worker explicitly (and repeat when its code/config changes):", ` ${context.packageManager ?? "npm"} run deploy:response-store`, "vinext-cloudflare deploy only deploys the application Worker.");
205
206
  if (cloudflare.prerender && cloudflare.cdnCache !== "static-assets") nextSteps.push("Pre-rendered routes are built, but Cloudflare deploys do not serve them.", " Use the Static Assets cache to serve them, or cache warming to fill another cache.");
206
207
  nextSteps.push("For TypeScript, add \".cloudflare/types\" to the include list in tsconfig.json.", "Worker types are generated during dev/build; run `cf workers types` before standalone type-checks.");
@@ -280,6 +281,21 @@ function resolveWorkerEntry(root) {
280
281
  return "vinext/server/fetch-handler";
281
282
  }
282
283
  /**
284
+ * A `worker/index.*` that never reaches vinext's handler (say, a migrated Vite
285
+ * SPA's API Worker) answers every page route itself, so each one 404s (#4460).
286
+ */
287
+ function workerEntryWarning(root, isStaticExport) {
288
+ if (isStaticExport) return [];
289
+ const entry = resolveWorkerEntry(root);
290
+ if (entry === "vinext/server/fetch-handler") return [];
291
+ if (fs.readFileSync(path.join(root, entry), "utf-8").includes("vinext/server/fetch-handler")) return [];
292
+ return [
293
+ `Warning: ${entry.slice(2)} does not import vinext/server/fetch-handler. As the Worker entry`,
294
+ " it must hand requests to vinext, or every page 404s. Otherwise point the Worker entry",
295
+ " (entrypoint in cloudflare.config.ts, or main in wrangler.jsonc) at vinext/server/fetch-handler."
296
+ ];
297
+ }
298
+ /**
283
299
  * Next.js runs no middleware for a static export, and an export with
284
300
  * `trailingSlash` relies on the default html_handling to serve `/about/` from
285
301
  * `about/index.html`, so export apps keep asset-first serving.
@@ -0,0 +1,18 @@
1
+ import { Plugin } from "vite";
2
+ //#region src/plugins/css-target.d.ts
3
+ /**
4
+ * Applies {@link DEFAULT_CSS_TARGET} only where nobody chose a target (#4413).
5
+ *
6
+ * Vite merges config by concatenating arrays, and the CSS minifier keeps the
7
+ * oldest version of each browser, so a pin merged onto a chosen target would
8
+ * stop an app from ever raising it. Instead nothing is set while config hooks
9
+ * run, so the app and every plugin hook see and merge plain Vite values. The
10
+ * post-ordered `config` and `configEnvironment` hooks only note where a target
11
+ * was set, and the pin is applied once config is resolved.
12
+ *
13
+ * Both the top-level `build`, which Vite's CSS plugin minifies with (a copy of
14
+ * the environment's in per-environment builds), and every environment's `build`
15
+ * are checked.
16
+ */
17
+ export declare function createCssTargetPlugin(): Plugin;
18
+ //#endregion
@@ -0,0 +1,93 @@
1
+ //#region src/plugins/css-target.ts
2
+ /**
3
+ * CSS minification target. Vite defaults it to the same target as JS
4
+ * (modern evergreens), which lets the CSS minifier (Lightning CSS by
5
+ * default, or esbuild) rewrite
6
+ * `@media (max-width: 768px)` to the Media Queries Level 4 range
7
+ * syntax `@media (width <= 768px)`. Both forms are semantically
8
+ * equivalent in modern browsers, but the rewrite is observable to
9
+ * user code that inspects `cssText` of `CSSMediaRule`s and breaks
10
+ * tools that pattern-match the raw query string. Next.js does not
11
+ * perform this rewrite by default (its webpack/lightningcss CSS
12
+ * pipeline preserves the original syntax), so user code carried
13
+ * over from Next.js can break when migrating to vinext.
14
+ *
15
+ * The minifier lowers a CSS feature when ANY target in the list lacks
16
+ * support, so we only need to pin one engine below the range-syntax
17
+ * baseline. Range syntax shipped in Chrome 104, Edge 104, Firefox 63,
18
+ * and Safari 16.4 (per esbuild's `internal/compat/css_table.go`
19
+ * MediaRange entry — and caniuse). Of those, Safari is the latest:
20
+ * pinning `safari15` (semantically Safari 15.0, which predates
21
+ * Safari 16.4) is sufficient to suppress the rewrite on its own.
22
+ *
23
+ * The other three targets are pinned to ~2023 baselines instead of
24
+ * the absolute oldest supported version so the minifier does NOT
25
+ * collaterally downlevel unrelated modern CSS features. With
26
+ * chrome111/edge111/firefox114 we keep through:
27
+ * - `:is()` pseudo-class (Chrome 88, Firefox 78)
28
+ * - `hwb()` colors (Chrome 101, Firefox 96)
29
+ * - `lab()`, `oklch()`, `color()` (Chrome 111, Firefox 113)
30
+ * - gradient interpolation hints (Chrome 111)
31
+ * CSS Nesting (Chrome 120, Safari 17.2) and Firefox-137 gradient
32
+ * interpolation will still be lowered; that is an intentional
33
+ * trade-off — those features are newer than the baseline and
34
+ * lowering them is the correct behavior for our target audience.
35
+ *
36
+ * Mirrors the Next.js fixture
37
+ * test/e2e/app-dir/css-media-query/css-media-query.test.ts which
38
+ * asserts `cssText` preserves `max-width: 768px`.
39
+ */
40
+ const DEFAULT_CSS_TARGET = [
41
+ "chrome111",
42
+ "edge111",
43
+ "firefox114",
44
+ "safari15"
45
+ ];
46
+ /**
47
+ * Applies {@link DEFAULT_CSS_TARGET} only where nobody chose a target (#4413).
48
+ *
49
+ * Vite merges config by concatenating arrays, and the CSS minifier keeps the
50
+ * oldest version of each browser, so a pin merged onto a chosen target would
51
+ * stop an app from ever raising it. Instead nothing is set while config hooks
52
+ * run, so the app and every plugin hook see and merge plain Vite values. The
53
+ * post-ordered `config` and `configEnvironment` hooks only note where a target
54
+ * was set, and the pin is applied once config is resolved.
55
+ *
56
+ * Both the top-level `build`, which Vite's CSS plugin minifies with (a copy of
57
+ * the environment's in per-environment builds), and every environment's `build`
58
+ * are checked.
59
+ */
60
+ function createCssTargetPlugin() {
61
+ let topLevelConfigured = false;
62
+ const configuredEnvironments = /* @__PURE__ */ new Set();
63
+ return {
64
+ name: "vinext:css-target",
65
+ enforce: "post",
66
+ config: {
67
+ order: "post",
68
+ handler(config) {
69
+ topLevelConfigured = config.build?.cssTarget !== void 0;
70
+ configuredEnvironments.clear();
71
+ }
72
+ },
73
+ configEnvironment: {
74
+ order: "post",
75
+ handler(name, config) {
76
+ if (config.build?.cssTarget !== void 0) configuredEnvironments.add(name);
77
+ }
78
+ },
79
+ configResolved: {
80
+ order: "pre",
81
+ handler(config) {
82
+ pinUnsetCssTarget(config.build, topLevelConfigured);
83
+ for (const [name, environment] of Object.entries(config.environments)) pinUnsetCssTarget(environment.build, configuredEnvironments.has(name));
84
+ }
85
+ }
86
+ };
87
+ }
88
+ function pinUnsetCssTarget(build, configured) {
89
+ if (configured || build.cssTarget !== build.target) return;
90
+ build.cssTarget = [...DEFAULT_CSS_TARGET];
91
+ }
92
+ //#endregion
93
+ export { createCssTargetPlugin };
@@ -109,6 +109,10 @@ type ParallelSlot = {
109
109
  loadingTreePositions?: number[];
110
110
  /** Absolute path to the slot's error component */
111
111
  errorPath: string | null;
112
+ /** Every error convention from the slot root to its active page, outermost first. */
113
+ errorPaths?: string[];
114
+ /** Slot-root-relative tree positions aligned with errorPaths. */
115
+ errorTreePositions?: number[];
112
116
  /** Nearest not-found convention for the slot's active branch. */
113
117
  notFoundPath?: string | null;
114
118
  /** Tree position of that convention relative to the slot root. */
@@ -167,6 +171,12 @@ export type AppRoute = {
167
171
  id: string;
168
172
  ownerTreePath: string;
169
173
  state: "active" | "default" | "unmatched";
174
+ /**
175
+ * Set on a sub-route minted for a slot page with no children page of its
176
+ * own: its children render a default, a sibling catch-all, or nothing, so
177
+ * its route segments don't describe them.
178
+ */
179
+ synthetic?: true;
170
180
  };
171
181
  /**
172
182
  * Interception markers not wrapped in an `@slot` directory.
@@ -594,7 +594,8 @@ function discoverSlotSubRoutes(routes, matcher, ghostParents = []) {
594
594
  childrenSlot: {
595
595
  id: childrenSlotId,
596
596
  ownerTreePath: childrenOwnerTreePath,
597
- state: childrenDefault ? "default" : childrenCatchAll ? "active" : "unmatched"
597
+ state: childrenDefault ? "default" : childrenCatchAll ? "active" : "unmatched",
598
+ synthetic: true
598
599
  },
599
600
  loadingPath: null,
600
601
  loadingPaths: parentRoute.loadingPaths,
@@ -742,10 +743,22 @@ function withSlotLoadingEntries(slot, pagePath, matcher) {
742
743
  ...slot,
743
744
  loadingPaths: loadingEntries.map((entry) => entry.path),
744
745
  loadingTreePositions: loadingEntries.map((entry) => entry.treePosition),
746
+ ...findSlotErrorFields(slot.ownerDir, pagePath, matcher),
745
747
  ...findSlotNotFoundFields(slot.ownerDir, pagePath, matcher)
746
748
  };
747
749
  }
748
750
  /**
751
+ * Every error convention on the slot's active branch. Each catches errors in
752
+ * its own segment, below that segment's layout.
753
+ */
754
+ function findSlotErrorFields(slotDir, pagePath, matcher) {
755
+ const entries = findSlotSegmentFileEntries(slotDir, pagePath, "error", matcher);
756
+ return {
757
+ errorPaths: entries.map((entry) => entry.path),
758
+ errorTreePositions: entries.map((entry) => entry.treePosition)
759
+ };
760
+ }
761
+ /**
749
762
  * Every not-found convention on the slot's active branch, plus the nearest one,
750
763
  * which supplies the slot's not-found metadata.
751
764
  */
@@ -1347,6 +1360,7 @@ function discoverParallelSlots(dir, appDir, matcher, includeNestedOnlySlots = fa
1347
1360
  loadingPaths: loadingEntries.map((loading) => loading.path),
1348
1361
  loadingTreePositions: loadingEntries.map((loading) => loading.treePosition),
1349
1362
  errorPath: findFile(slotDir, "error", matcher),
1363
+ ...findSlotErrorFields(slotDir, pagePath, matcher),
1350
1364
  ...findSlotNotFoundFields(slotDir, pagePath, matcher),
1351
1365
  interceptingRoutes,
1352
1366
  layoutIndex: -1,