Files
cloudflare__vinext/knip.ts
T
James Anderson 1c0bdacda6 feat(apps/web): /compatibility page backed by D1 + deploy-suite ingest (#1239)
* feat(apps/web): /compatibility page backed by D1 + deploy-suite ingest

Adds a /compatibility page to apps/web that visualises Next.js
compatibility over time, populated by the nightly deploy-suite workflow.

  - D1 binding (`vinext-metrics`) with Drizzle schema and migrations.
    Tables are namespaced (`compat_runs`, `compat_file_results`) so
    future metric kinds can live alongside.
  - POST /api/compatibility ingest endpoint. Authenticates with a
    `COMPAT_INGEST_SECRET` worker secret and upserts by
    (kind, runKey).
  - GitHub-style contribution grid (one dot per test file) and a
    pass-rate-over-time line chart. Both are SVG, no chart libs.
    Pass rate excludes Next.js-skipped tests from the denominator.
  - Shared header/footer extracted into app/_components and rendered
    by the root layout. Nav uses next/link for soft client nav.
  - Workflow change: on top of the existing per-test pass/fail/skip
    aggregation, the report job now POSTs results to the ingest
    endpoint. Guarded to only run when (a) filter=all and
    (b) targeting main, so partial runs and branch spot-checks don't
    pollute the historical record.

The first nightly run after merge will seed the production D1.

* address review + add ISR with KV cache

Review fixes (apps/web/app/api/compatibility/route.ts):
  - constant-time secret comparison via SHA-256 + bytewise XOR
  - single-statement atomic upsert with onConflictDoUpdate (no
    SELECT->INSERT race for concurrent retried runs)
  - top-level try/catch with structured error body so workflow logs
    show why ingest failed instead of a generic 500
  - MAX_FILES=2000 cap in validation as defensive bound

Page perf (apps/web/app/compatibility/page.tsx):
  - latest-run and trend D1 queries run in parallel via Promise.all

ISR + KV:
  - new vinext-web-cache KV namespace bound as VINEXT_CACHE
  - worker installs KVCacheHandler once per isolate
  - both pages opt into ISR with revalidate = 300; compat page drops
    force-dynamic

* address bonk re-review

  - Drop the ?kind= query param. Only one kind exists today; gating it
    behind an unused param invited ISR cache pollution. Hardcoded to
    "deploy" with a comment pointing future kinds at a dedicated route.

  - Wrap the DELETE + chunked INSERTs in a single `db.batch()` so a
    crash mid-write can no longer leave a run with zero file results.
    D1 executes batch statements inside one transaction.

  - Resolve workflow expressions once via step-level `env:` and read
    them as `process.env.*` from the github-script body. Removes the
    fragile splicing of `${{ inputs.next-ref || 'v16.2.6' }}` into JS
    string literals (the nested single quotes were the worst offender).
2026-05-15 15:10:36 +00:00

126 lines
5.3 KiB
TypeScript

import { readFileSync } from "node:fs";
import type { KnipConfig } from "knip";
function entriesFromPackageJson(relativePath: string): string[] {
const pkg = JSON.parse(readFileSync(new URL(relativePath, import.meta.url), "utf8")) as {
bin?: string | Record<string, string>;
exports?: Record<string, unknown>;
};
const targets = new Set<string>();
const visit = (value: unknown) => {
if (typeof value === "string") targets.add(value);
else if (value && typeof value === "object") for (const v of Object.values(value)) visit(v);
};
visit(pkg.bin);
visit(pkg.exports);
return [...targets]
.filter((t) => t.endsWith(".js"))
.map((t) =>
t
.replace(/^\.\//, "")
.replace(/^dist\//, "src/")
.replace(/\.js$/, ".{ts,tsx}"),
);
}
export default {
workspaces: {
".": {
entry: ["scripts/*.{js,ts}", "tests/**/*.test.ts", "tests/helpers.ts"],
project: ["scripts/**/*.{js,ts}", "tests/**/*.{js,ts}", "!tests/fixtures/**"],
},
"packages/vinext": {
entry: [
...entriesFromPackageJson("packages/vinext/package.json"),
// Build-time entries referenced by path constant (Vite reads them
// from disk via `fs`), so knip wouldn't otherwise trace them.
"src/server/app-browser-entry.ts",
"src/server/app-ssr-entry.ts",
// Runtime helpers imported by generated virtual entries. The imports
// are emitted as strings, so knip cannot trace them statically.
"src/server/app-middleware.ts",
"src/server/app-page-dispatch.ts",
"src/server/app-page-head.ts",
"src/server/app-prerender-static-params.ts",
// Client-side instrumentation bundle: loaded as a side-effect module
// by the generated hydration entries (import "vinext/instrumentation-client"),
// so its public surface (clientInstrumentationHooks, getClientInstrumentationHooks)
// is consumed by the user's app at runtime, not by imports knip can follow.
"src/client/instrumentation-client.ts",
"src/client/instrumentation-client-state.ts",
// Shims for `next/*` internal modules — their exports are consumed by
// type-only imports in third-party packages' .d.ts files (e.g.
// @clerk/nextjs, @sentry/nextjs, nextjs-toploader). Those imports
// originate in node_modules and are invisible to knip.
"src/shims/internal/api-utils.ts",
"src/shims/internal/app-router-context.ts",
"src/shims/internal/utils.ts",
// Typed WorkUnitStore exports consumed by cache.ts via AsyncLocalStorage
// generic — knip cannot trace type-only dependencies through ALS.
"src/shims/internal/work-unit-async-storage.ts",
// Imported via template string in app-rsc-entry.ts (generated code),
// so knip cannot trace the import statically.
"src/server/prerender-work-unit-setup.ts",
"src/server/app-page-element-builder.ts",
"src/server/app-hook-warning-suppression.ts",
"src/server/app-post-middleware-context.ts",
"src/server/app-request-context.ts",
"src/server/app-rsc-error-handler.ts",
"src/server/rsc-stream-hints.ts",
// #726-CACHE-01/04 defines the disabled proof boundary before runtime
// observation recording or cache reuse is wired in later slices.
"src/server/cache-proof.ts",
],
project: ["src/**/*.{ts,tsx}"],
},
},
ignoreWorkspaces: ["examples/**", "tests/fixtures/**", "benchmarks/**"],
ignoreDependencies: [
"@typescript/native-preview",
// Declared at root package.json but imported from workspace/example code:
// @mdx-js/react — no direct imports; retained for MDX runtime resolution.
// @mdx-js/rollup — imported from examples/app-router-playground/vite.config.ts
// which doesn't declare it locally and relies on root hoisting.
"@mdx-js/react",
"@mdx-js/rollup",
// probed via require.resolve
"next-intl",
// Optional peer dep used by Vite's built-in SCSS preprocessor when the
// user installs it. `tests/scss.test.ts` dynamically imports it to skip
// the SCSS suite when sass is absent, so we never list it as a hard dep.
"sass",
// vitest reporter used outside CI
...(process.env.CI ? [] : ["agent"]),
// internal module name, not an actual dependency
"private-next-instrumentation-client",
// Cloudflare Workers runtime virtual module — provides `env` for
// accessing wrangler bindings. Not an npm package. Knip strips the
// `cloudflare:workers` specifier down to the bare scheme.
"cloudflare",
],
ignoreBinaries: [
// workspace's own bin, invoked in CI
"vinext",
],
ignoreFiles: [
"tests/e2e/app-router/nextjs-compat/playwright.nextjs-compat.config.ts",
// stub module loaded via `path.resolve()` as a Vite alias target
"packages/vinext/src/client/empty-module.ts",
],
// Catalog check is noisy here (deps consumed from the workspace's own
// sub-packages or `examples/**` which we intentionally ignore).
// `duplicates` flags intentional compat aliases — see
// packages/vinext/src/shims/internal/work-unit-async-storage.ts where
// `requestAsyncStorage` is a legacy alias for `workUnitAsyncStorage`.
exclude: ["catalog", "duplicates"],
} satisfies KnipConfig;