mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
54497d42e7
* fix(pages): send charset=utf-8 on HTML Content-Type Next.js serves every HTML response (SSR and prerendered) with `Content-Type: text/html; charset=utf-8`, but vinext's Pages Router paths sent a bare `text/html`. Without the header charset — and without an early <meta charset> in the page — Chromium falls back to windows-1252, so non-ASCII content renders as mojibake (e.g. nbsp as 'Â ') and the resulting DOM diverges from the hydrated tree, triggering the full-screen hydration error overlay in dev. Reproduced against nextjs-notion-starter-kit; baseline `next dev`/`next start` both send the charset. Append `; charset=utf-8` on every Pages Router HTML producer (the App Router paths already send it): - pages-page-response.ts — page render response headers + gSSP header merge - dev-server.ts — streaming SSR, ISR HIT/STALE, and static-HTML dev responses - pages-page-data.ts — ISR cache HIT/STALE responses in prod - pages-request-pipeline.ts — the `defaultContentType` a buffering adapter applies when a render response carries no Content-Type - static-file-cache.ts — `.html` static files (prerendered pages), matching Next.js static serving Compression negotiation is unaffected: COMPRESSIBLE_TYPES matching splits the media type on ";" before lookup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(client): polyfill `global` in browser bundles Next.js exposes the Node-style `global` alias to client code: webpack via its `node.global` runtime shim, Turbopack by compile-time rewriting the free `global` identifier to its globalThis shortcut and folding `typeof global` to "object" (turbopack-ecmascript references). vinext provided nothing, so any client dependency that reads `global` (e.g. use-dark-mode via nextjs-notion-starter-kit) threw `ReferenceError: global is not defined` after hydration. Add a `vinext:client-global-define` plugin that scopes `define: { global: "globalThis" }` to the client environment: - builds statically rewrite free `global` references (Turbopack-style) - dev injects `"global": globalThis` into the client runtime defines (/@vite/env), assigning `globalThis.global` before user code runs (webpack-style) - the same define is layered into the client dep optimizer (rolldownOptions.transform.define / esbuildOptions.define) because pre-bundled deps bypass the plugin transform pipeline `typeof global` evaluates to "object" in the browser either way, matching Next.js. Server environments are untouched — `global` remains the real Node global — and a user-configured `compiler.define.global` takes precedence, mirroring Turbopack's or_insert free-var semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(images): accept /_next/image/ with trailingSlash: true With `trailingSlash: true`, the App Router dev handler 308-redirects `/_next/image?url=...` to `/_next/image/?url=...` (the trailing-slash normalizer runs before the image-endpoint check), but the image endpoint only matched the exact `/_next/image` pathname — so the redirected request 404'd and every dev-mode next/image request broke. Reproduced on tailwind-nextjs-starter-blog, which ships trailingSlash: true. Baseline Next 16.2.10 behaves the same way up to the redirect (dev with trailingSlash: true also 308s `/_next/image` to `/_next/image/`) but then SERVES the slashed form: its route matching strips a trailing slash before matching internal paths (getItem in packages/next/src/server/lib/router-utils/filesystem.ts), so image requests never fail. Match that: isImageOptimizationPath() now strips a single trailing slash before comparing, which covers every caller — App Router dev/prod (app-rsc-handler), Pages Router dev middleware, the Node prod server, and the Cloudflare worker entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: align existing assertions with charset and trailing-slash fixes Three assertions added on main after these fixes were authored still asserted the old behavior: bare text/html Content-Type in static-file-cache and the pages pipeline defaultContentType, and isImageOptimizationPath rejecting the trailing-slash form. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
268 lines
11 KiB
TypeScript
268 lines
11 KiB
TypeScript
import path from "node:path";
|
|
import { defineConfig } from "vite-plus";
|
|
import { randomUUID } from "node:crypto";
|
|
|
|
const SHIMS_SRC = path.resolve(import.meta.dirname, "packages/vinext/src/shims");
|
|
const VINEXT_SRC = path.resolve(import.meta.dirname, "packages/vinext/src");
|
|
const CLOUDFLARE_SRC = path.resolve(import.meta.dirname, "packages/cloudflare/src");
|
|
const MSW_SETUP = path.resolve(import.meta.dirname, "tests/_msw/setup.ts");
|
|
|
|
// Resolve own-workspace sources directly in tests so the vinext <->
|
|
// @vinext/cloudflare dependency edge points at source (single module instance,
|
|
// no prior build required). Shared by both test projects below.
|
|
const WORKSPACE_SRC_ALIAS = {
|
|
"vinext/shims": SHIMS_SRC,
|
|
"vinext/internal": VINEXT_SRC,
|
|
"@vinext/cloudflare/internal": CLOUDFLARE_SRC,
|
|
"@vinext/cloudflare/cache": path.resolve(import.meta.dirname, "packages/cloudflare/src/cache"),
|
|
"@vinext/cloudflare/images": path.resolve(import.meta.dirname, "packages/cloudflare/src/images"),
|
|
};
|
|
|
|
export default defineConfig({
|
|
staged: {
|
|
"*": "vp check --fix",
|
|
},
|
|
fmt: {
|
|
printWidth: 100,
|
|
tabWidth: 2,
|
|
useTabs: false,
|
|
semi: true,
|
|
singleQuote: false,
|
|
trailingComma: "all",
|
|
ignorePatterns: ["tests/fixtures/ecosystem/**", "examples/**"],
|
|
},
|
|
lint: {
|
|
ignorePatterns: [
|
|
"fixtures/ecosystem/**",
|
|
"tests/fixtures/**",
|
|
"tests/fixtures/ecosystem/**",
|
|
"examples/**",
|
|
],
|
|
options: {
|
|
typeAware: true,
|
|
typeCheck: true,
|
|
denyWarnings: true,
|
|
},
|
|
plugins: ["typescript", "unicorn", "import", "react"],
|
|
jsPlugins: [
|
|
"./oxlint-plugins/prefer-import-alias.js",
|
|
"./oxlint-plugins/prefer-shared-utils.js",
|
|
],
|
|
rules: {
|
|
"@typescript-eslint/no-explicit-any": "error",
|
|
"typescript/consistent-type-definitions": ["error", "type"],
|
|
"@typescript-eslint/no-unsafe-function-type": "error",
|
|
"@typescript-eslint/no-unused-vars": "error",
|
|
"@typescript-eslint/no-redeclare": "error",
|
|
"@typescript-eslint/no-implied-eval": "error",
|
|
|
|
"unicorn/prefer-node-protocol": "error",
|
|
|
|
"import/first": "error",
|
|
"import/no-duplicates": "error",
|
|
|
|
"no-new-func": "error",
|
|
"no-implied-eval": "error",
|
|
"arrow-body-style": ["error", "as-needed"],
|
|
|
|
"react/exhaustive-deps": "error",
|
|
"react/no-array-index-key": "error",
|
|
"react/rules-of-hooks": "error",
|
|
"react/self-closing-comp": "error",
|
|
"vinext-utils/prefer-shared-utils": "error",
|
|
},
|
|
overrides: [
|
|
{
|
|
files: ["**.spec.ts", "**.test.ts"],
|
|
rules: {
|
|
"@typescript-eslint/no-explicit-any": "off",
|
|
"@typescript-eslint/no-unsafe-function-type": "off",
|
|
"typescript/no-misused-promises": "error",
|
|
"typescript/switch-exhaustiveness-check": "error",
|
|
"import/no-self-import": "error",
|
|
"unicorn/throw-new-error": "error",
|
|
"unicorn/error-message": "error",
|
|
},
|
|
},
|
|
{
|
|
files: ["packages/vinext/src/**/*.{ts,tsx}"],
|
|
rules: {
|
|
"typescript/no-misused-promises": "error",
|
|
"typescript/switch-exhaustiveness-check": "error",
|
|
"typescript/restrict-template-expressions": "error",
|
|
"import/no-self-import": "error",
|
|
"unicorn/throw-new-error": "error",
|
|
"unicorn/error-message": "error",
|
|
},
|
|
},
|
|
{
|
|
// Forces relative imports of own-package files inside vinext to use
|
|
// the tsconfig path alias (e.g. ../shims/X.js → vinext/shims/X).
|
|
// Originally added for #1001 — bare specifiers keep
|
|
// @vitejs/plugin-rsc's `packageSources` map populated, which avoids
|
|
// the broken absolute-fs-path proxy fallback.
|
|
files: ["packages/vinext/**"],
|
|
rules: {
|
|
"vinext-local/prefer-import-alias": "error",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
test: {
|
|
// GitHub Actions reporter adds inline failure annotations in PR diffs.
|
|
// Agent reporter suppresses passing test noise when running inside AI agents.
|
|
reporters: process.env.CI ? ["default", "github-actions"] : ["default", "agent"],
|
|
|
|
// Shared env for all projects.
|
|
env: {
|
|
// Mirrors the Vite `define` in index.ts that inlines a build-time UUID.
|
|
// Setting it here means tests exercise the same code path as production.
|
|
__VINEXT_DRAFT_SECRET: randomUUID(),
|
|
},
|
|
|
|
// Coverage activated only via --coverage flag (or CLI/config override).
|
|
// Istanbul provider is required because vinext source loads through Vite's
|
|
// module runner (helpers.ts:15 imports from packages/vinext/src directly,
|
|
// and integration tests start in-process Vite servers via createServer()).
|
|
// V8 coverage misses code under that runner.
|
|
coverage: {
|
|
provider: "istanbul",
|
|
include: ["packages/vinext/src/**/*.{ts,tsx}"],
|
|
exclude: ["**/*.d.ts", "**/*.test.ts", "**/*.spec.ts"],
|
|
reporter: ["text", "html", "json-summary", "lcov"],
|
|
reportsDirectory: "coverage",
|
|
clean: true,
|
|
skipFull: true,
|
|
// Always emit the report, even if some test files failed. A failing
|
|
// integration test (e.g. timing-sensitive teardown) shouldn't suppress
|
|
// coverage data we already collected.
|
|
reportOnFailure: true,
|
|
},
|
|
|
|
projects: [
|
|
{
|
|
resolve: {
|
|
alias: WORKSPACE_SRC_ALIAS,
|
|
},
|
|
test: {
|
|
name: "unit",
|
|
setupFiles: [MSW_SETUP],
|
|
// `scripts/**` covers the release-tooling unit tests
|
|
// (scripts/create-changeset.test.ts, scripts/version.test.ts), which
|
|
// are pure-logic and have no fixture/server dependencies.
|
|
include: ["tests/**/*.test.ts", "scripts/**/*.test.ts"],
|
|
exclude: [
|
|
"tests/fixtures/**/node_modules/**",
|
|
// Integration tests: spin up Vite dev servers against shared fixture
|
|
// dirs. Must run serially to avoid Vite deps optimizer cache races
|
|
// (node_modules/.vite/*) that produce "outdated pre-bundle" 500s.
|
|
// When adding a test that calls startFixtureServer() or createServer(),
|
|
// move it here.
|
|
"tests/app-router-client-preloading.test.ts",
|
|
"tests/app-router-deployment-id.test.ts",
|
|
"tests/app-router-dev-server.test.ts",
|
|
"tests/app-router-external-rewrite.test.ts",
|
|
"tests/app-router-font-google-prod.test.ts",
|
|
"tests/app-router-isr-codegen.test.ts",
|
|
"tests/app-router-malformed-url.test.ts",
|
|
"tests/app-router-metadata-routes.test.ts",
|
|
"tests/app-router-middleware-next-request.test.ts",
|
|
"tests/app-router-next-config-codegen.test.ts",
|
|
"tests/app-router-next-config-dev.test.ts",
|
|
"tests/app-router-origin-check.test.ts",
|
|
"tests/app-router-proxy-conventions.test.ts",
|
|
"tests/app-router-production-build.test.ts",
|
|
"tests/app-router-production-server.test.ts",
|
|
"tests/app-router-rsc-flight-hint.test.ts",
|
|
"tests/app-router-rsc-plugin.test.ts",
|
|
"tests/app-router-static-export.test.ts",
|
|
"tests/app-router-worker-entry.test.ts",
|
|
"tests/api-handler.test.ts",
|
|
"tests/cjs.test.ts",
|
|
"tests/client-global-define.test.ts",
|
|
"tests/ecosystem.test.ts",
|
|
"tests/entry-templates.test.ts",
|
|
"tests/features.test.ts",
|
|
"tests/favicon-short-circuit.test.ts",
|
|
"tests/image-optimization-parity.test.ts",
|
|
"tests/node-modules-css.test.ts",
|
|
"tests/pages-i18n-prod.test.ts",
|
|
"tests/pages-router-concurrency.test.ts",
|
|
"tests/pages-router.test.ts",
|
|
"tests/postcss-resolve.test.ts",
|
|
"tests/prerender.test.ts",
|
|
"tests/static-export.test.ts",
|
|
"tests/vite-hmr-websocket.test.ts",
|
|
"tests/nextjs-compat/**/*.test.ts",
|
|
// Flaky under parallelism due to 1 MiB buffer allocation pressure.
|
|
"tests/kv-cache-handler.test.ts",
|
|
],
|
|
},
|
|
},
|
|
{
|
|
resolve: {
|
|
alias: WORKSPACE_SRC_ALIAS,
|
|
},
|
|
test: {
|
|
name: "integration",
|
|
// MSW is intentionally NOT installed in the integration project.
|
|
// Integration tests spin up in-process HTTP servers and fixture
|
|
// dev servers and exercise them via `fetch("http://127.0.0.1:<port>/...")`.
|
|
// The @mswjs/interceptors layer interferes with that loopback
|
|
// traffic in subtle ways even when handlers `passthrough()`
|
|
// (e.g. 5xx response bodies stall, fixture-startup readiness
|
|
// probes time out). MSW's value here is mocking external HTTP
|
|
// for unit tests of fetch wrappers — integration tests already
|
|
// talk to real local servers, not to the network, so the
|
|
// unhandled-request guard buys little. If a future integration
|
|
// test needs to mock an external fetch, wire MSW per-file with
|
|
// `setupServer` rather than reverting this exclusion.
|
|
include: [
|
|
"tests/app-router-client-preloading.test.ts",
|
|
"tests/app-router-deployment-id.test.ts",
|
|
"tests/app-router-dev-server.test.ts",
|
|
"tests/app-router-external-rewrite.test.ts",
|
|
"tests/app-router-font-google-prod.test.ts",
|
|
"tests/app-router-isr-codegen.test.ts",
|
|
"tests/app-router-malformed-url.test.ts",
|
|
"tests/app-router-metadata-routes.test.ts",
|
|
"tests/app-router-middleware-next-request.test.ts",
|
|
"tests/app-router-next-config-codegen.test.ts",
|
|
"tests/app-router-next-config-dev.test.ts",
|
|
"tests/app-router-origin-check.test.ts",
|
|
"tests/app-router-proxy-conventions.test.ts",
|
|
"tests/app-router-production-build.test.ts",
|
|
"tests/app-router-production-server.test.ts",
|
|
"tests/app-router-rsc-flight-hint.test.ts",
|
|
"tests/app-router-rsc-plugin.test.ts",
|
|
"tests/app-router-static-export.test.ts",
|
|
"tests/app-router-worker-entry.test.ts",
|
|
"tests/api-handler.test.ts",
|
|
"tests/cjs.test.ts",
|
|
"tests/client-global-define.test.ts",
|
|
"tests/ecosystem.test.ts",
|
|
"tests/entry-templates.test.ts",
|
|
"tests/favicon-short-circuit.test.ts",
|
|
"tests/features.test.ts",
|
|
"tests/image-optimization-parity.test.ts",
|
|
"tests/kv-cache-handler.test.ts",
|
|
"tests/node-modules-css.test.ts",
|
|
"tests/pages-i18n-prod.test.ts",
|
|
"tests/pages-router-concurrency.test.ts",
|
|
"tests/pages-router.test.ts",
|
|
"tests/postcss-resolve.test.ts",
|
|
"tests/prerender.test.ts",
|
|
"tests/static-export.test.ts",
|
|
"tests/vite-hmr-websocket.test.ts",
|
|
"tests/nextjs-compat/**/*.test.ts",
|
|
],
|
|
testTimeout: 30000,
|
|
// Serial execution prevents Vite deps optimizer cache races when
|
|
// multiple test files share the same fixture directory.
|
|
fileParallelism: false,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|