@wular/pnext 0.0.1
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.
- package/README.md +153 -0
- package/bin/pnext +67 -0
- package/config/lint/base.js +48 -0
- package/config/ts/base.json +26 -0
- package/config/ts/react.json +13 -0
- package/package.json +70 -0
- package/reference/compat.md +63 -0
- package/reference/config.md +120 -0
- package/reference/css.md +58 -0
- package/reference/dev.md +69 -0
- package/reference/env.md +40 -0
- package/reference/metadata.md +86 -0
- package/reference/navigation.md +149 -0
- package/reference/overview.md +35 -0
- package/reference/performance.md +97 -0
- package/reference/rendering.md +127 -0
- package/reference/routing.md +167 -0
- package/reference/typegen.md +64 -0
- package/src/api/cache.ts +80 -0
- package/src/api/client-cache.ts +9 -0
- package/src/api/client-navigation.ts +279 -0
- package/src/api/dynamic.tsx +102 -0
- package/src/api/link.tsx +119 -0
- package/src/api/navigation.ts +198 -0
- package/src/api/router/events.ts +53 -0
- package/src/api/router/history.ts +70 -0
- package/src/api/router/hub.ts +194 -0
- package/src/api/router/policies.ts +107 -0
- package/src/api/router/runtime.ts +5238 -0
- package/src/api/router/types.ts +299 -0
- package/src/api/router.ts +167 -0
- package/src/api/server.ts +323 -0
- package/src/api/suspense.ts +16 -0
- package/src/cache/context.ts +61 -0
- package/src/cli/adapters/vercel-warm.ts +375 -0
- package/src/cli/adapters/vercel.ts +1310 -0
- package/src/cli/analyze-print.ts +181 -0
- package/src/cli/analyze.ts +328 -0
- package/src/cli/boot-trace.ts +29 -0
- package/src/cli/build.ts +3114 -0
- package/src/cli/dev.ts +276 -0
- package/src/cli/index.ts +196 -0
- package/src/cli/named-bin.ts +119 -0
- package/src/cli/serve-ui.ts +160 -0
- package/src/cli/start.ts +1425 -0
- package/src/client/build.ts +2136 -0
- package/src/client/chunk-fold.ts +526 -0
- package/src/client/entry.ts +1525 -0
- package/src/client/paths.ts +22 -0
- package/src/client/prebuilt.ts +621 -0
- package/src/client/profile.ts +75 -0
- package/src/client/react-compiler.ts +94 -0
- package/src/client/reference-stub.ts +145 -0
- package/src/client/reference.ts +47 -0
- package/src/compat/actions/action-client.ts +531 -0
- package/src/compat/actions/action-dispatch.ts +676 -0
- package/src/compat/actions/action-router.ts +40 -0
- package/src/compat/actions/action-shared.ts +95 -0
- package/src/compat/actions/client-plugin.ts +135 -0
- package/src/compat/actions/client-stub.ts +65 -0
- package/src/compat/actions/config.ts +164 -0
- package/src/compat/actions/detect.ts +208 -0
- package/src/compat/actions/discovery.ts +244 -0
- package/src/compat/actions/early-submit.ts +40 -0
- package/src/compat/actions/endpoint.ts +602 -0
- package/src/compat/actions/flight.ts +52 -0
- package/src/compat/actions/form-state.ts +73 -0
- package/src/compat/actions/hoist.ts +485 -0
- package/src/compat/actions/ids.ts +39 -0
- package/src/compat/actions/index.ts +41 -0
- package/src/compat/actions/instances.ts +144 -0
- package/src/compat/actions/origin.ts +109 -0
- package/src/compat/actions/protocol.ts +125 -0
- package/src/compat/actions/registry.ts +74 -0
- package/src/compat/actions/rewrite.ts +282 -0
- package/src/compat/actions/serve.ts +414 -0
- package/src/compat/actions/server-tag.ts +21 -0
- package/src/compat/actions/unrecognized-error.ts +30 -0
- package/src/compat/adapter/build-complete.ts +257 -0
- package/src/compat/bundler/bun-externals.ts +53 -0
- package/src/compat/bundler/cjs-exports.ts +542 -0
- package/src/compat/bundler/config.ts +363 -0
- package/src/compat/bundler/externals.ts +34 -0
- package/src/compat/bundler/import-meta-url.ts +60 -0
- package/src/compat/bundler/modularize-imports.ts +119 -0
- package/src/compat/bundler/new-url-asset.ts +87 -0
- package/src/compat/bundler/optimize-package-imports.ts +273 -0
- package/src/compat/bundler/polyfill.ts +88 -0
- package/src/compat/bundler/react-compiler.ts +61 -0
- package/src/compat/bundler/react-profiler.tsx +25 -0
- package/src/compat/bundler/relay-transform.ts +116 -0
- package/src/compat/bundler/require-context.ts +281 -0
- package/src/compat/bundler/resolve-extensions.ts +75 -0
- package/src/compat/bundler/source-cache.ts +61 -0
- package/src/compat/bundler/static-imports.ts +25 -0
- package/src/compat/bundler/symlink-imports.ts +119 -0
- package/src/compat/bundler/tsconfig-paths.ts +50 -0
- package/src/compat/bundler/wasm.ts +153 -0
- package/src/compat/bundler/webpack-loaders.ts +685 -0
- package/src/compat/bundler/worker.ts +278 -0
- package/src/compat/cache/build-flags.ts +81 -0
- package/src/compat/cache/build-prerender-errors.ts +163 -0
- package/src/compat/cache/custom-handler.ts +159 -0
- package/src/compat/cache/fetch-patch.ts +745 -0
- package/src/compat/cache/handler.ts +100 -0
- package/src/compat/cache/modern-handler.ts +275 -0
- package/src/compat/cache/resume-data-cache.ts +143 -0
- package/src/compat/cache/revalidate.ts +759 -0
- package/src/compat/cache/runtime-error.ts +124 -0
- package/src/compat/cache/use-cache-transform.ts +961 -0
- package/src/compat/cache/use-cache.ts +1695 -0
- package/src/compat/cache-control.ts +269 -0
- package/src/compat/client/base-path.ts +64 -0
- package/src/compat/client/css-order.ts +36 -0
- package/src/compat/client/errors/control-flow.ts +92 -0
- package/src/compat/client/errors/error-boundary.ts +222 -0
- package/src/compat/client/errors/global-error.ts +238 -0
- package/src/compat/client/errors/install.ts +217 -0
- package/src/compat/client/errors/lazy.ts +53 -0
- package/src/compat/client/errors/primitive-throw.ts +126 -0
- package/src/compat/client/errors/soft-refresh.ts +14 -0
- package/src/compat/client/link-status.ts +86 -0
- package/src/compat/client/nav-compat-runtime.ts +57 -0
- package/src/compat/client/nav-compat.ts +42 -0
- package/src/compat/client/navigation-scroll.ts +154 -0
- package/src/compat/client/optimistic-routing.ts +206 -0
- package/src/compat/client/prefetch-cache.ts +111 -0
- package/src/compat/client/route-announcer.ts +72 -0
- package/src/compat/client/segment-cache-policy.ts +159 -0
- package/src/compat/client/segment-cache.ts +1077 -0
- package/src/compat/client/segment-prefetch.ts +375 -0
- package/src/compat/client/trailing-slash.ts +24 -0
- package/src/compat/css/chunking.ts +254 -0
- package/src/compat/css/inline-css.ts +73 -0
- package/src/compat/css/lightningcss.ts +90 -0
- package/src/compat/css/modules.ts +373 -0
- package/src/compat/css/nonce.ts +30 -0
- package/src/compat/css/sass-plugin.ts +65 -0
- package/src/compat/css/sass.ts +392 -0
- package/src/compat/css/styled-jsx-runtime.ts +80 -0
- package/src/compat/css/styled-jsx.ts +49 -0
- package/src/compat/edge-runtime.ts +71 -0
- package/src/compat/export/client.ts +112 -0
- package/src/compat/export/index.ts +272 -0
- package/src/compat/export/standalone.ts +207 -0
- package/src/compat/image-optimizer/cache.ts +119 -0
- package/src/compat/image-optimizer/detect.ts +143 -0
- package/src/compat/image-optimizer/index.ts +601 -0
- package/src/compat/image-optimizer/source.ts +243 -0
- package/src/compat/index.ts +458 -0
- package/src/compat/lifecycle/after-scope.ts +86 -0
- package/src/compat/lifecycle/after.ts +173 -0
- package/src/compat/lifecycle/error-funnel.ts +306 -0
- package/src/compat/lifecycle/error-serialize.ts +87 -0
- package/src/compat/lifecycle/error-ui.ts +167 -0
- package/src/compat/lifecycle/instrumentation-client.ts +138 -0
- package/src/compat/lifecycle/instrumentation.ts +277 -0
- package/src/compat/lifecycle/node-console.ts +19 -0
- package/src/compat/lifecycle/testmode.ts +263 -0
- package/src/compat/mdx/compile.ts +219 -0
- package/src/compat/mdx/next-mdx-stub.ts +46 -0
- package/src/compat/mdx/plugin.ts +37 -0
- package/src/compat/metadata-route-artifacts.ts +458 -0
- package/src/compat/metadata.ts +295 -0
- package/src/compat/middleware/manifest.ts +210 -0
- package/src/compat/misc/action-return.ts +173 -0
- package/src/compat/next/cache.ts +211 -0
- package/src/compat/next/canonical-url.ts +35 -0
- package/src/compat/next/client-cache.ts +57 -0
- package/src/compat/next/client-navigation.ts +313 -0
- package/src/compat/next/client-only.ts +3 -0
- package/src/compat/next/client-script.tsx +215 -0
- package/src/compat/next/client-server.ts +39 -0
- package/src/compat/next/config-loader.ts +569 -0
- package/src/compat/next/config.ts +29 -0
- package/src/compat/next/constants.cjs +6 -0
- package/src/compat/next/constants.ts +6 -0
- package/src/compat/next/custom-server.ts +236 -0
- package/src/compat/next/dist/client/components/app-router-headers.ts +32 -0
- package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +38 -0
- package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -0
- package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -0
- package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -0
- package/src/compat/next/dynamic.tsx +46 -0
- package/src/compat/next/error.tsx +148 -0
- package/src/compat/next/font/cache.ts +171 -0
- package/src/compat/next/font/google.ts +2 -0
- package/src/compat/next/font/index.ts +8 -0
- package/src/compat/next/font/local.ts +5 -0
- package/src/compat/next/font/runtime-client.ts +71 -0
- package/src/compat/next/font/runtime.ts +974 -0
- package/src/compat/next/font/shared.ts +281 -0
- package/src/compat/next/form.tsx +156 -0
- package/src/compat/next/head.tsx +10 -0
- package/src/compat/next/headers.ts +247 -0
- package/src/compat/next/image/config.ts +196 -0
- package/src/compat/next/image/optimizer.ts +96 -0
- package/src/compat/next/image/patterns.ts +103 -0
- package/src/compat/next/image/shared.ts +141 -0
- package/src/compat/next/image/static-metadata.ts +283 -0
- package/src/compat/next/image/validate.ts +269 -0
- package/src/compat/next/image-client.tsx +215 -0
- package/src/compat/next/image-props.ts +575 -0
- package/src/compat/next/image-usage.ts +102 -0
- package/src/compat/next/image.tsx +56 -0
- package/src/compat/next/index.ts +1 -0
- package/src/compat/next/legacy-image.tsx +97 -0
- package/src/compat/next/link-usage.ts +29 -0
- package/src/compat/next/link-validation-transform.ts +200 -0
- package/src/compat/next/link.tsx +466 -0
- package/src/compat/next/navigation.cjs +21 -0
- package/src/compat/next/navigation.ts +188 -0
- package/src/compat/next/offline.ts +51 -0
- package/src/compat/next/og.ts +324 -0
- package/src/compat/next/optimistic-route-state.ts +188 -0
- package/src/compat/next/preferred-region.ts +39 -0
- package/src/compat/next/redirects.ts +131 -0
- package/src/compat/next/resource-hints.ts +136 -0
- package/src/compat/next/rewrites.ts +350 -0
- package/src/compat/next/root-params.ts +142 -0
- package/src/compat/next/router.cjs +49 -0
- package/src/compat/next/router.ts +143 -0
- package/src/compat/next/script.tsx +355 -0
- package/src/compat/next/server-only.ts +3 -0
- package/src/compat/next/server.ts +28 -0
- package/src/compat/next/svgr.ts +58 -0
- package/src/compat/next/telemetry.ts +77 -0
- package/src/compat/next/user-agent.ts +100 -0
- package/src/compat/next/web-vitals.ts +56 -0
- package/src/compat/otel/api.ts +95 -0
- package/src/compat/otel/client-trace-metadata.ts +71 -0
- package/src/compat/otel/fetch-span.ts +77 -0
- package/src/compat/otel/tracer.ts +944 -0
- package/src/compat/pages/client-plugin.ts +108 -0
- package/src/compat/pages/index.ts +527 -0
- package/src/compat/pages/router-state.ts +94 -0
- package/src/compat/ppr/io.ts +38 -0
- package/src/compat/ppr/missing-root-params.ts +105 -0
- package/src/compat/ppr/root-params-scan.ts +164 -0
- package/src/compat/ppr/root-params-transform.ts +75 -0
- package/src/compat/ppr/root-params.ts +129 -0
- package/src/compat/ppr/segment-config-incompat.ts +34 -0
- package/src/compat/protocol.ts +202 -0
- package/src/compat/react/client.ts +59 -0
- package/src/compat/react/compiler-runtime.ts +60 -0
- package/src/compat/react/dom-client.ts +115 -0
- package/src/compat/react/dom-react-server.ts +20 -0
- package/src/compat/react/dom-server.ts +40 -0
- package/src/compat/react/dom.ts +154 -0
- package/src/compat/react/preact.ts +522 -0
- package/src/compat/react/react-server.ts +84 -0
- package/src/compat/react/router-shim.ts +26 -0
- package/src/compat/react/server-component-use.ts +48 -0
- package/src/compat/react/server-inserted-html.ts +87 -0
- package/src/compat/react/server.ts +156 -0
- package/src/compat/react/view-transition.ts +60 -0
- package/src/compat/register/actions.ts +875 -0
- package/src/compat/register/boot.ts +141 -0
- package/src/compat/register/build-tier.ts +11 -0
- package/src/compat/register/build.ts +182 -0
- package/src/compat/register/bundler.ts +587 -0
- package/src/compat/register/cache.ts +85 -0
- package/src/compat/register/client-errors.ts +18 -0
- package/src/compat/register/config.ts +17 -0
- package/src/compat/register/css-extras.ts +120 -0
- package/src/compat/register/edge-runtime.ts +6 -0
- package/src/compat/register/errors.ts +46 -0
- package/src/compat/register/export.ts +23 -0
- package/src/compat/register/font.ts +36 -0
- package/src/compat/register/hooks.ts +34 -0
- package/src/compat/register/image.ts +133 -0
- package/src/compat/register/index.ts +111 -0
- package/src/compat/register/instrumentation-client.ts +35 -0
- package/src/compat/register/lifecycle.ts +86 -0
- package/src/compat/register/mdx.ts +48 -0
- package/src/compat/register/middleware.ts +36 -0
- package/src/compat/register/misc.ts +44 -0
- package/src/compat/register/otel.ts +288 -0
- package/src/compat/register/pages-api.ts +473 -0
- package/src/compat/register/ppr.ts +56 -0
- package/src/compat/register/protocol.ts +57 -0
- package/src/compat/register/proxy.ts +127 -0
- package/src/compat/register/render.ts +268 -0
- package/src/compat/register/routing.ts +410 -0
- package/src/compat/register/segment.ts +1903 -0
- package/src/compat/register/static-image.ts +21 -0
- package/src/compat/register/typed-routes.ts +35 -0
- package/src/compat/register/usecache.ts +131 -0
- package/src/compat/register/validation.ts +56 -0
- package/src/compat/segment/loading-boundary.ts +113 -0
- package/src/compat/segment/page-slot.ts +200 -0
- package/src/compat/segment/tree.ts +481 -0
- package/src/compat/segment/vary-key.ts +102 -0
- package/src/compat/segment/vary-params.ts +551 -0
- package/src/compat/static-params.ts +33 -0
- package/src/compat/tsconfig-defaults.ts +301 -0
- package/src/compat/typecheck/index.ts +1481 -0
- package/src/compat/typecheck/worker.ts +26 -0
- package/src/compat/typed-routes/index.ts +92 -0
- package/src/compat/typed-routes/manifest.ts +356 -0
- package/src/compat/typed-routes/typegen.ts +566 -0
- package/src/compat/validation/errors.ts +159 -0
- package/src/compat/validation/index.ts +1770 -0
- package/src/compat/validation/prerender-diagnostics.ts +1508 -0
- package/src/compat-bootstrap.ts +67 -0
- package/src/config.ts +218 -0
- package/src/css/build.ts +697 -0
- package/src/css/index.ts +2 -0
- package/src/css/postcss.ts +236 -0
- package/src/css/worker.ts +34 -0
- package/src/dev/client-actions.ts +35 -0
- package/src/dev/client-chunk-store.ts +92 -0
- package/src/dev/client-key-cache.ts +178 -0
- package/src/dev/global-css-cache.ts +212 -0
- package/src/dev/imports.ts +2430 -0
- package/src/dev/module-cache.ts +721 -0
- package/src/dev/module-generations.ts +38 -0
- package/src/dev/module-transform.ts +188 -0
- package/src/dev/node-module-bundle-cache.ts +63 -0
- package/src/dev/restart-cache.ts +10 -0
- package/src/dev/route-bundle-key-cache.ts +154 -0
- package/src/dev/route-facts-cache.ts +223 -0
- package/src/dev/server.ts +1710 -0
- package/src/dynamic/source.ts +307 -0
- package/src/dynamic/tree-shake.ts +262 -0
- package/src/env.ts +92 -0
- package/src/extensions.ts +1898 -0
- package/src/index.ts +34 -0
- package/src/internal.ts +43 -0
- package/src/islands/boundary-error.ts +8 -0
- package/src/islands/static-children.ts +37 -0
- package/src/islands/static-slots.ts +106 -0
- package/src/ppr-postpone.ts +24 -0
- package/src/ppr.ts +784 -0
- package/src/proxy.ts +752 -0
- package/src/render/hooks.ts +384 -0
- package/src/render/index.ts +1 -0
- package/src/render/island-context.ts +47 -0
- package/src/render/metadata.ts +857 -0
- package/src/render/renderer.ts +7391 -0
- package/src/render/resource-hints.ts +44 -0
- package/src/render/slots.tsx +679 -0
- package/src/request/context.ts +396 -0
- package/src/resolve/engine.ts +219 -0
- package/src/resolve/imports.ts +1104 -0
- package/src/resolve/scan-facts.ts +474 -0
- package/src/resolve/source-text.ts +86 -0
- package/src/routing/forwarded.ts +41 -0
- package/src/routing/handler.ts +271 -0
- package/src/routing/href.ts +203 -0
- package/src/routing/metadata.ts +1018 -0
- package/src/routing/request-runtime.ts +43 -0
- package/src/routing/routes.ts +2560 -0
- package/src/routing/slots.ts +432 -0
- package/src/runtime/server.ts +3453 -0
- package/src/runtime/vendor.ts +1160 -0
- package/src/style-modules.d.ts +9 -0
- package/src/typegen.ts +151 -0
- package/src/types.ts +725 -0
- package/src/utils/ansi.ts +9 -0
- package/src/utils/content-type.ts +31 -0
- package/src/utils/decode.ts +7 -0
- package/src/utils/dev-profile.ts +31 -0
- package/src/utils/error-log.ts +29 -0
- package/src/utils/fs-cache.ts +31 -0
- package/src/utils/fs.ts +119 -0
- package/src/utils/html.ts +46 -0
- package/src/utils/serialize.ts +378 -0
- package/src/utils/source.ts +35 -0
- package/src/utils/verbose.ts +39 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// The app's postcss/Tailwind pipeline. Lives in its own module because it runs
|
|
2
|
+
// on the CSS worker thread (./worker.ts) rather than the event loop:
|
|
3
|
+
// Tailwind v4's postcss plugin is synchronous CPU work — its oxide scan and
|
|
4
|
+
// compile starved every concurrent request while it ran in-process.
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { writeFile } from 'node:fs/promises';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { pathToFileURL } from 'node:url';
|
|
10
|
+
import { readText } from '../utils/fs';
|
|
11
|
+
|
|
12
|
+
export interface PostcssOptions {
|
|
13
|
+
dev?: boolean;
|
|
14
|
+
/** pnext's output directory — kept out of Tailwind's content scan. */
|
|
15
|
+
outDir?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// One long-lived processor per root+mode. Keeping it alive is what makes dev fast: Tailwind v4's postcss
|
|
19
|
+
// plugin holds a compiler and an oxide scanner per input file, so rebuilds are incremental candidate scans
|
|
20
|
+
// instead of a CLI spawn plus a full project re-scan. It also means other plugins in the config actually
|
|
21
|
+
// execute - the old CLI path treated postcss.config purely as a Tailwind trigger. Config-file edits need a
|
|
22
|
+
// dev-server restart; Tailwind tracks its own dependencies and refreshes them itself.
|
|
23
|
+
const postcssProcessors = new Map<string, Promise<LoadedPostcss | undefined>>();
|
|
24
|
+
|
|
25
|
+
interface PostcssProcessor {
|
|
26
|
+
process(css: string, options: { from: string; to: string }): PromiseLike<{ css: string }>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A processor plus the `@source not` directives to append for it (if any). */
|
|
30
|
+
interface LoadedPostcss {
|
|
31
|
+
processor: PostcssProcessor;
|
|
32
|
+
sources: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type PostcssPluginFactory = (options?: unknown) => unknown;
|
|
36
|
+
|
|
37
|
+
interface PostcssConfig {
|
|
38
|
+
plugins?: Record<string, unknown> | unknown[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function postcssConfigFile(root: string) {
|
|
42
|
+
for (const file of ['postcss.config.cjs', 'postcss.config.js', 'postcss.config.mjs']) {
|
|
43
|
+
const full = path.join(root, file);
|
|
44
|
+
if (existsSync(full)) return full;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Build (or reuse) the processor for this root+mode without processing a file. */
|
|
50
|
+
export function loadPostcss(root: string, options: PostcssOptions) {
|
|
51
|
+
const key = `${root}\0${options.dev ? 'dev' : 'build'}\0${options.outDir ?? ''}`;
|
|
52
|
+
let processor = postcssProcessors.get(key);
|
|
53
|
+
if (!processor) {
|
|
54
|
+
processor = createPostcssProcessor(root, options).catch((error: unknown) => {
|
|
55
|
+
// Drop failed setups so a fixed config or install is retried.
|
|
56
|
+
postcssProcessors.delete(key);
|
|
57
|
+
throw error;
|
|
58
|
+
});
|
|
59
|
+
postcssProcessors.set(key, processor);
|
|
60
|
+
}
|
|
61
|
+
return processor;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function runPostcss(
|
|
65
|
+
root: string,
|
|
66
|
+
cssFile: string,
|
|
67
|
+
options: PostcssOptions,
|
|
68
|
+
fromSource?: string,
|
|
69
|
+
) {
|
|
70
|
+
const loaded = await loadPostcss(root, options);
|
|
71
|
+
if (!loaded) return;
|
|
72
|
+
// `from` labels the input for postcss plugins' diagnostics/source maps. Point
|
|
73
|
+
// it at the original CSS source (not the synthesized bundle outfile) so
|
|
74
|
+
// plugins like `@tailwindcss/postcss` report the real path (e.g. its
|
|
75
|
+
// `DEBUG=tailwindcss` marker keys off `result.opts.from`). We still write the
|
|
76
|
+
// processed output back to the bundle outfile via `to`.
|
|
77
|
+
const from = fromSource ?? cssFile;
|
|
78
|
+
// The scan directives go last so they don't shift the line numbers plugins
|
|
79
|
+
// report against the real source; `@source` is order-independent to Tailwind.
|
|
80
|
+
const result = await loaded.processor.process((await readText(cssFile)) + loaded.sources, {
|
|
81
|
+
from,
|
|
82
|
+
to: cssFile,
|
|
83
|
+
});
|
|
84
|
+
await writeFile(cssFile, result.css);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function createPostcssProcessor(root: string, options: PostcssOptions) {
|
|
88
|
+
const configFile = postcssConfigFile(root);
|
|
89
|
+
if (!configFile) return undefined;
|
|
90
|
+
const loaded = (await import(pathToFileURL(configFile).href)) as
|
|
91
|
+
| PostcssConfig
|
|
92
|
+
| { default: PostcssConfig };
|
|
93
|
+
const config = 'default' in loaded && loaded.default ? loaded.default : loaded;
|
|
94
|
+
const plugins = postcssPlugins(root, config as PostcssConfig, options);
|
|
95
|
+
if (plugins.length === 0) return undefined;
|
|
96
|
+
const appRequire = createRequire(path.join(root, 'package.json'));
|
|
97
|
+
const postcss = interopDefault(resolvePostcss(appRequire)) as (
|
|
98
|
+
plugins: unknown[],
|
|
99
|
+
) => PostcssProcessor;
|
|
100
|
+
const tailwind = pluginEntries(config as PostcssConfig).some(
|
|
101
|
+
([specifier, pluginOptions]) => specifier === '@tailwindcss/postcss' && pluginOptions !== false,
|
|
102
|
+
);
|
|
103
|
+
return {
|
|
104
|
+
processor: postcss(plugins),
|
|
105
|
+
sources: tailwind ? scanExclusions(root, appRequire, options) : '',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Tailwind's automatic content detection only skips dependencies and build output when it can read
|
|
111
|
+
* the project's git ignore rules. Off a git checkout - Docker build contexts, staged copies,
|
|
112
|
+
* tarball deploys - oxide walks them instead and mints utilities from stale build artifacts.
|
|
113
|
+
* Excluding these three by name restores that behaviour without narrowing what a correctly-ignored
|
|
114
|
+
* checkout already scans. Apps that really do keep Tailwind-classed sources in one of them set
|
|
115
|
+
* `PNEXT_TAILWIND_SCAN_ALL`.
|
|
116
|
+
*/
|
|
117
|
+
function scanExclusions(root: string, appRequire: NodeJS.Require, options: PostcssOptions) {
|
|
118
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
119
|
+
if (process.env.PNEXT_TAILWIND_SCAN_ALL) return '';
|
|
120
|
+
// `@source not` landed in Tailwind 4.1; older versions error on the at-rule.
|
|
121
|
+
const [major, minor] = packageVersion(appRequire, '@tailwindcss/postcss');
|
|
122
|
+
if (major < 4 || (major === 4 && minor < 1)) return '';
|
|
123
|
+
const dirs = ['node_modules', '.next', options.outDir ?? '.pnext'];
|
|
124
|
+
const excluded = [...new Set(dirs.map(dir => path.resolve(root, dir)))].filter(
|
|
125
|
+
dir => dir.startsWith(root + path.sep) && existsSync(dir),
|
|
126
|
+
);
|
|
127
|
+
if (excluded.length === 0) return '';
|
|
128
|
+
// Leading and trailing newlines keep the stylesheet's own first and last
|
|
129
|
+
// lines exactly as they were, so the emitted CSS stays byte-identical.
|
|
130
|
+
return `\n${excluded
|
|
131
|
+
.map(dir => `@source not ${JSON.stringify(dir.split(path.sep).join('/'))};`)
|
|
132
|
+
.join('\n')}\n`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** `[major, minor]` of a resolvable package, or `[0, 0]` when unknown. */
|
|
136
|
+
function packageVersion(req: NodeJS.Require, name: string): [number, number] {
|
|
137
|
+
try {
|
|
138
|
+
const pkg = JSON.parse(readFileSync(req.resolve(`${name}/package.json`), 'utf8')) as {
|
|
139
|
+
version?: string;
|
|
140
|
+
};
|
|
141
|
+
const [major, minor] = (pkg.version ?? '').split('.');
|
|
142
|
+
return [Number.parseInt(major ?? '', 10) || 0, Number.parseInt(minor ?? '', 10) || 0];
|
|
143
|
+
} catch {
|
|
144
|
+
return [0, 0];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function pluginEntries(config: PostcssConfig): [unknown, unknown][] {
|
|
149
|
+
return Array.isArray(config.plugins)
|
|
150
|
+
? config.plugins.map(entry =>
|
|
151
|
+
Array.isArray(entry)
|
|
152
|
+
? ([entry[0], entry[1]] as [unknown, unknown])
|
|
153
|
+
: ([entry, undefined] as [unknown, unknown]),
|
|
154
|
+
)
|
|
155
|
+
: Object.entries(config.plugins ?? {});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function postcssPlugins(root: string, config: PostcssConfig, options: PostcssOptions) {
|
|
159
|
+
const appRequire = createRequire(path.join(root, 'package.json'));
|
|
160
|
+
const entries = pluginEntries(config);
|
|
161
|
+
|
|
162
|
+
const plugins: unknown[] = [];
|
|
163
|
+
for (const [specifier, pluginOptions] of entries) {
|
|
164
|
+
if (pluginOptions === false) continue;
|
|
165
|
+
if (typeof specifier !== 'string') {
|
|
166
|
+
// Already-instantiated plugin object or factory in an array config.
|
|
167
|
+
if (specifier) plugins.push(specifier);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const factory = interopDefault(appRequire(specifier) as unknown);
|
|
171
|
+
const resolvedOptions = pluginOptionsFor(root, specifier, pluginOptions, options);
|
|
172
|
+
plugins.push(
|
|
173
|
+
typeof factory === 'function' ? (factory as PostcssPluginFactory)(resolvedOptions) : factory,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
return plugins;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function pluginOptionsFor(
|
|
180
|
+
root: string,
|
|
181
|
+
specifier: string,
|
|
182
|
+
userOptions: unknown,
|
|
183
|
+
options: PostcssOptions,
|
|
184
|
+
) {
|
|
185
|
+
if (specifier !== '@tailwindcss/postcss') return userOptions;
|
|
186
|
+
// Scan candidates from the app root — the bundled css Tailwind reads lives
|
|
187
|
+
// under .pnext, where a default input-relative scan would find nothing.
|
|
188
|
+
return {
|
|
189
|
+
base: root,
|
|
190
|
+
optimize: options.dev ? false : { minify: true },
|
|
191
|
+
...(typeof userOptions === 'object' && userOptions !== null ? userOptions : {}),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function resolvePostcss(appRequire: NodeJS.Require): unknown {
|
|
196
|
+
// Next runs the PostCSS pipeline through its OWN bundled PostCSS 8, never the app's copy. That matters
|
|
197
|
+
// when a legacy plugin dep pins an old major: `postcss-nested@4` declares `postcss@^7` as a regular
|
|
198
|
+
// dependency, which the package manager hoists to the app's top-level node_modules, so requiring
|
|
199
|
+
// 'postcss' from the app would return PostCSS 7 and a modern app plugin using the v8 API throws. Prefer
|
|
200
|
+
// the app's postcss only when it is already v8+, otherwise fall back to pnext's own.
|
|
201
|
+
let appPostcss: unknown;
|
|
202
|
+
try {
|
|
203
|
+
appPostcss = appRequire('postcss');
|
|
204
|
+
if (packageVersion(appRequire, 'postcss')[0] >= 8) return appPostcss;
|
|
205
|
+
} catch {
|
|
206
|
+
// App has no resolvable postcss; fall through to pnext's own / plugin copy.
|
|
207
|
+
}
|
|
208
|
+
const ownPostcss = resolveOwnPostcss();
|
|
209
|
+
if (ownPostcss) return ownPostcss;
|
|
210
|
+
try {
|
|
211
|
+
// A Tailwind-based app ships a v8 postcss under its plugin package.
|
|
212
|
+
const pluginPackage = appRequire.resolve('@tailwindcss/postcss/package.json');
|
|
213
|
+
return createRequire(pluginPackage)('postcss');
|
|
214
|
+
} catch {
|
|
215
|
+
// Last resort: whatever the app resolved (even if <8) — better than nothing.
|
|
216
|
+
if (appPostcss) return appPostcss;
|
|
217
|
+
throw new Error('Could not resolve a PostCSS installation for the CSS pipeline.');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** pnext's own PostCSS 8 (mirrors Next's bundled copy), or undefined. */
|
|
222
|
+
function resolveOwnPostcss(): unknown {
|
|
223
|
+
try {
|
|
224
|
+
const ownRequire = createRequire(import.meta.url);
|
|
225
|
+
if (packageVersion(ownRequire, 'postcss')[0] >= 8) return ownRequire('postcss');
|
|
226
|
+
} catch {
|
|
227
|
+
// pnext's install carries no resolvable postcss; caller falls back.
|
|
228
|
+
}
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function interopDefault(mod: unknown): unknown {
|
|
233
|
+
if (typeof mod === 'function') return mod;
|
|
234
|
+
if (mod && typeof mod === 'object' && 'default' in mod && mod.default) return mod.default;
|
|
235
|
+
return mod;
|
|
236
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// CSS worker entry — runs the postcss/Tailwind pipeline off the event loop.
|
|
2
|
+
// One message per built stylesheet; the processor cache in ./postcss.ts lives
|
|
3
|
+
// here for the worker's lifetime, so Tailwind stays incremental across rebuilds.
|
|
4
|
+
import { loadPostcss, runPostcss } from './postcss';
|
|
5
|
+
export type CssWorkerRequest = { id: number; root: string; dev: boolean; outDir?: string } & (
|
|
6
|
+
| { kind: 'warm' }
|
|
7
|
+
| { kind: 'process'; cssFile: string; from?: string }
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
export interface CssWorkerResponse {
|
|
11
|
+
id: number;
|
|
12
|
+
error?: { name: string; message: string; stack?: string };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
declare const self: Worker;
|
|
16
|
+
|
|
17
|
+
self.onmessage = async (event: MessageEvent<CssWorkerRequest>) => {
|
|
18
|
+
const request = event.data;
|
|
19
|
+
const reply = (response: CssWorkerResponse) => postMessage(response);
|
|
20
|
+
try {
|
|
21
|
+
const options = { dev: request.dev, outDir: request.outDir };
|
|
22
|
+
if (request.kind === 'warm') await loadPostcss(request.root, options);
|
|
23
|
+
else await runPostcss(request.root, request.cssFile, options, request.from);
|
|
24
|
+
reply({ id: request.id });
|
|
25
|
+
} catch (error) {
|
|
26
|
+
reply({
|
|
27
|
+
id: request.id,
|
|
28
|
+
error:
|
|
29
|
+
error instanceof Error
|
|
30
|
+
? { name: error.name, message: error.message, stack: error.stack }
|
|
31
|
+
: { name: 'Error', message: String(error) },
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Client-action bundler seam (CORE - imports ZERO compat).
|
|
2
|
+
//
|
|
3
|
+
// The dev route-client bundle resolves every module into its own `pnext-route-client` esbuild namespace, so
|
|
4
|
+
// a standalone esbuild plugin cannot intercept that graph the way it can a plain client build. This holder
|
|
5
|
+
// is the extension point dev/imports reads to turn a client-side import of a `'use server'` module into the
|
|
6
|
+
// RPC stub, without core statically importing compat/actions.
|
|
7
|
+
//
|
|
8
|
+
// Compat populates it; a pure-core app leaves it unset, and dev/imports treats every module as a normal
|
|
9
|
+
// source module. Mirrors the routing/request-runtime.ts holder pattern.
|
|
10
|
+
|
|
11
|
+
/** Compat client-action bundler surface dev/imports consults. */
|
|
12
|
+
export interface ClientActionBundler {
|
|
13
|
+
/**
|
|
14
|
+
* Ensure the client-action module set is discovered for the active dev import version (a dev reload bumps
|
|
15
|
+
* it). Awaited before a route-client build so the stub check below sees the current set - the discovery
|
|
16
|
+
* that used to run inline at startup/reload now happens lazily here. Idempotent per version.
|
|
17
|
+
*/
|
|
18
|
+
ensureArmed: () => Promise<void>;
|
|
19
|
+
/** True when `file` is a discovered `'use server'` module (stub it on the client). */
|
|
20
|
+
isClientActionModule: (file: string) => boolean;
|
|
21
|
+
/** Generate the client fetch-stub source for an action module file. */
|
|
22
|
+
stubSource: (file: string) => Promise<string>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let bundler: ClientActionBundler | undefined;
|
|
26
|
+
|
|
27
|
+
/** Publish the compat client-action bundler (register-actions). */
|
|
28
|
+
export function setClientActionBundler(next: ClientActionBundler): void {
|
|
29
|
+
bundler = next;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The compat client-action bundler, or undefined for a pure-core app. */
|
|
33
|
+
export function getClientActionBundler(): ClientActionBundler | undefined {
|
|
34
|
+
return bundler;
|
|
35
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Content-addressed store for dev client build output. Generation dirs under
|
|
2
|
+
// cache/client/<key>/ repeat the same chunk bytes across saves (~42% of
|
|
3
|
+
// cache/client measured byte-duplicate on a zero-edit warm-up).
|
|
4
|
+
// Hardlinking each file into a shared store keyed by its own hash collapses
|
|
5
|
+
// duplicates to one inode while every generation dir keeps a normal-looking
|
|
6
|
+
// path; eviction of one generation just drops its link, leaving the others
|
|
7
|
+
// (and the store's own link) intact.
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { link, mkdir, readFile, readdir, rm, stat, unlink } from 'node:fs/promises';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { listFiles } from '../utils/fs';
|
|
12
|
+
|
|
13
|
+
export function clientChunkStoreDir(outPath: string) {
|
|
14
|
+
return path.join(outPath, 'cache', 'client-store');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function contentHash(data: Buffer) {
|
|
18
|
+
return createHash('sha1').update(data).digest('hex');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Replace every file under `dir` with a hardlink into the shared store, deduping
|
|
23
|
+
* identical content written by unrelated generations. Falls back to a plain
|
|
24
|
+
* copy when linking fails (e.g. store and outDir on different devices) so the
|
|
25
|
+
* file always ends up in place either way.
|
|
26
|
+
*/
|
|
27
|
+
export async function contentAddressDir(dir: string, storeDir: string) {
|
|
28
|
+
await mkdir(storeDir, { recursive: true });
|
|
29
|
+
const files = await listFiles(dir);
|
|
30
|
+
await Promise.all(files.map(file => contentAddressFile(file, storeDir)));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function contentAddressFile(file: string, storeDir: string) {
|
|
34
|
+
const data = await readFile(file);
|
|
35
|
+
const storePath = path.join(storeDir, `${contentHash(data)}${path.extname(file)}`);
|
|
36
|
+
if (!(await pathExists(storePath))) {
|
|
37
|
+
// Claim the store slot from this file's own bytes: link first (cheap, same
|
|
38
|
+
// device), and only fall back to a copy if that's impossible. Either way the
|
|
39
|
+
// original file stays at `file` — nothing below needs to re-link it there.
|
|
40
|
+
try {
|
|
41
|
+
await link(file, storePath);
|
|
42
|
+
return;
|
|
43
|
+
} catch {
|
|
44
|
+
await copyFileFallback(file, storePath);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Store already holds this content: swap the freshly-written file for a link
|
|
49
|
+
// to the shared inode so duplicate bytes stop being duplicate on disk.
|
|
50
|
+
try {
|
|
51
|
+
await unlink(file);
|
|
52
|
+
await link(storePath, file);
|
|
53
|
+
} catch {
|
|
54
|
+
// Cross-device or racing unlink: leave the original file in place, it is
|
|
55
|
+
// already correct content, just not deduped this time.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function copyFileFallback(file: string, storePath: string) {
|
|
60
|
+
try {
|
|
61
|
+
const data = await readFile(file);
|
|
62
|
+
await Bun.write(storePath, data);
|
|
63
|
+
} catch {
|
|
64
|
+
// Best-effort: dedup is an optimization, not a correctness requirement.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function pathExists(target: string) {
|
|
69
|
+
return stat(target)
|
|
70
|
+
.then(() => true)
|
|
71
|
+
.catch(() => false);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Sweep store entries no generation dir links to anymore. A store file's link count is 1 exactly when the
|
|
76
|
+
* store's own hardlink is the only one left - every generation that once pointed at it has been evicted.
|
|
77
|
+
*/
|
|
78
|
+
export async function sweepClientChunkStore(storeDir: string) {
|
|
79
|
+
let entries: string[];
|
|
80
|
+
try {
|
|
81
|
+
entries = await readdir(storeDir);
|
|
82
|
+
} catch {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
await Promise.allSettled(
|
|
86
|
+
entries.map(async entry => {
|
|
87
|
+
const full = path.join(storeDir, entry);
|
|
88
|
+
const info = await stat(full).catch(() => null);
|
|
89
|
+
if (info && info.nlink <= 1) await rm(full, { force: true });
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// Persisted client cache keys - the restart half of dev/module-cache.ts. A route's client bundle
|
|
2
|
+
// already lives in a content-keyed out-dir, so a restart never has to rebuild it; deriving that key
|
|
3
|
+
// did, by walking every source the route's client graph reaches and hashing the bytes. The bytes
|
|
4
|
+
// cannot have moved while nothing was watching them *unless* the file did, so this persists the
|
|
5
|
+
// walk's result next to the artifacts it named and re-validates it with one stat per source.
|
|
6
|
+
//
|
|
7
|
+
// The same three-step ladder module-cache uses:
|
|
8
|
+
// stat matches -> reuse (the common case, and the only O(1) one)
|
|
9
|
+
// stat moved -> read + hash; equal content still reuses the key
|
|
10
|
+
// content differs -> full walk, new key, new out-dir
|
|
11
|
+
// so an edit made while the server was not running invalidates, and a touch that changed nothing
|
|
12
|
+
// does not.
|
|
13
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { mkdir, readFile, stat } from 'node:fs/promises';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import {
|
|
17
|
+
clientCacheKeyParts,
|
|
18
|
+
clientCacheStaticHash,
|
|
19
|
+
clientSourceHash,
|
|
20
|
+
type ClientCacheSource,
|
|
21
|
+
} from '../client/build';
|
|
22
|
+
import type { ResolvedConfig } from '../config';
|
|
23
|
+
import type { RouteManifestEntry } from '../types';
|
|
24
|
+
import { writeFileAtomic } from '../utils/fs';
|
|
25
|
+
import { restartCacheEnabled } from './restart-cache';
|
|
26
|
+
|
|
27
|
+
/** `[file, mtimeMs, size, contentHash]` — the same record shape the module index uses. */
|
|
28
|
+
type SourceEntry = [string, number, number, string];
|
|
29
|
+
|
|
30
|
+
interface RouteEntry {
|
|
31
|
+
/** Hash of everything in the key that is not a walked source. */
|
|
32
|
+
static: string;
|
|
33
|
+
key: string;
|
|
34
|
+
files: SourceEntry[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface IndexFile {
|
|
38
|
+
version: number;
|
|
39
|
+
routes: Record<string, RouteEntry>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Bumped whenever a record's shape changes; an older index is simply ignored. */
|
|
43
|
+
const INDEX_VERSION = 1;
|
|
44
|
+
|
|
45
|
+
interface KeyCache {
|
|
46
|
+
file: string;
|
|
47
|
+
routes: Map<string, RouteEntry>;
|
|
48
|
+
dirty: boolean;
|
|
49
|
+
flush?: Timer;
|
|
50
|
+
retries: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const caches = new Map<string, KeyCache>();
|
|
54
|
+
|
|
55
|
+
function keyCache(config: ResolvedConfig): KeyCache {
|
|
56
|
+
const file = path.join(config.outPath, 'cache', 'client', 'keys.json');
|
|
57
|
+
const existing = caches.get(file);
|
|
58
|
+
if (existing) return existing;
|
|
59
|
+
const created: KeyCache = { file, routes: readIndex(file), dirty: false, retries: 0 };
|
|
60
|
+
caches.set(file, created);
|
|
61
|
+
// A short-lived process can exit before the debounce fires; without this its
|
|
62
|
+
// walk would not survive to the next boot.
|
|
63
|
+
process.on('exit', () => {
|
|
64
|
+
if (!created.dirty) return;
|
|
65
|
+
created.dirty = false;
|
|
66
|
+
try {
|
|
67
|
+
writeFileSync(created.file, serializeIndex(created.routes));
|
|
68
|
+
} catch {
|
|
69
|
+
// Best effort: a missing index only costs the next boot a walk.
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
return created;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* This route's client cache key, from the persisted walk when every source it
|
|
77
|
+
* named still has the content it was named for, and from a fresh walk otherwise.
|
|
78
|
+
*/
|
|
79
|
+
export async function devClientCacheKey(
|
|
80
|
+
config: ResolvedConfig,
|
|
81
|
+
route: RouteManifestEntry,
|
|
82
|
+
nextCompat: boolean,
|
|
83
|
+
): Promise<string> {
|
|
84
|
+
if (!restartCacheEnabled()) return (await clientCacheKeyParts(route, nextCompat, config)).key;
|
|
85
|
+
const cache = keyCache(config);
|
|
86
|
+
const staticHash = await clientCacheStaticHash(route, nextCompat, config);
|
|
87
|
+
const entry = cache.routes.get(route.id);
|
|
88
|
+
if (entry?.static === staticHash) {
|
|
89
|
+
const revalidated = await revalidate(entry.files);
|
|
90
|
+
if (revalidated) {
|
|
91
|
+
// Only the stats moved (a touch, a checkout): keep the key, record the
|
|
92
|
+
// new stats so the next boot takes the O(1) path again.
|
|
93
|
+
if (revalidated !== entry.files) {
|
|
94
|
+
cache.routes.set(route.id, { ...entry, files: revalidated });
|
|
95
|
+
persist(cache);
|
|
96
|
+
}
|
|
97
|
+
return entry.key;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const parts = await clientCacheKeyParts(route, nextCompat, config, staticHash);
|
|
102
|
+
cache.routes.set(route.id, {
|
|
103
|
+
static: parts.staticHash,
|
|
104
|
+
key: parts.key,
|
|
105
|
+
files: parts.sources.map(sourceEntry),
|
|
106
|
+
});
|
|
107
|
+
persist(cache);
|
|
108
|
+
return parts.key;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function sourceEntry(source: ClientCacheSource): SourceEntry {
|
|
112
|
+
return [source.file, source.mtimeMs, source.size, source.hash];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The recorded sources, re-stated: `files` itself when nothing moved, a fresh list when only stats moved,
|
|
117
|
+
* `undefined` when any content actually differs - or a source is gone, which is the same thing, since the
|
|
118
|
+
* walk has to run again.
|
|
119
|
+
*/
|
|
120
|
+
async function revalidate(files: SourceEntry[]): Promise<SourceEntry[] | undefined> {
|
|
121
|
+
let moved = false;
|
|
122
|
+
const checked = await Promise.all(
|
|
123
|
+
files.map(async (entry): Promise<SourceEntry | undefined> => {
|
|
124
|
+
const [file, mtimeMs, size, hash] = entry;
|
|
125
|
+
const stats = await stat(file).catch(() => undefined);
|
|
126
|
+
if (!stats) return undefined;
|
|
127
|
+
if (stats.mtimeMs === mtimeMs && stats.size === size) return entry;
|
|
128
|
+
const source = await readFile(file, 'utf8').catch(() => undefined);
|
|
129
|
+
if (source === undefined || clientSourceHash(source) !== hash) return undefined;
|
|
130
|
+
moved = true;
|
|
131
|
+
return [file, stats.mtimeMs, stats.size, hash];
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
if (checked.some(entry => entry === undefined)) return undefined;
|
|
135
|
+
return moved ? (checked as SourceEntry[]) : files;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function persist(cache: KeyCache) {
|
|
139
|
+
cache.dirty = true;
|
|
140
|
+
if (cache.flush) return;
|
|
141
|
+
cache.flush = setTimeout(async () => {
|
|
142
|
+
cache.flush = undefined;
|
|
143
|
+
if (!cache.dirty) return;
|
|
144
|
+
const contents = serializeIndex(cache.routes);
|
|
145
|
+
try {
|
|
146
|
+
await mkdir(path.dirname(cache.file), { recursive: true });
|
|
147
|
+
await writeFileAtomic(cache.file, contents);
|
|
148
|
+
cache.dirty = false;
|
|
149
|
+
} catch {
|
|
150
|
+
// A write that lost a race with a cache wipe must not look persisted.
|
|
151
|
+
if (++cache.retries < 3) persist(cache);
|
|
152
|
+
}
|
|
153
|
+
}, 50);
|
|
154
|
+
// The index is a pure optimization; never hold the process open for it.
|
|
155
|
+
cache.flush.unref?.();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function readIndex(file: string) {
|
|
159
|
+
const routes = new Map<string, RouteEntry>();
|
|
160
|
+
let parsed: IndexFile | undefined;
|
|
161
|
+
try {
|
|
162
|
+
parsed = JSON.parse(readFileSync(file, 'utf8')) as IndexFile;
|
|
163
|
+
} catch {
|
|
164
|
+
return routes;
|
|
165
|
+
}
|
|
166
|
+
if (parsed?.version !== INDEX_VERSION) return routes;
|
|
167
|
+
for (const [id, entry] of Object.entries(parsed.routes ?? {})) {
|
|
168
|
+
if (entry?.key && entry.static && Array.isArray(entry.files)) routes.set(id, entry);
|
|
169
|
+
}
|
|
170
|
+
return routes;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function serializeIndex(routes: Map<string, RouteEntry>) {
|
|
174
|
+
return JSON.stringify({
|
|
175
|
+
version: INDEX_VERSION,
|
|
176
|
+
routes: Object.fromEntries(routes),
|
|
177
|
+
} satisfies IndexFile);
|
|
178
|
+
}
|