@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,974 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import type { ResolvedConfig } from '../../../config';
|
|
9
|
+
import { onExtensionHostReset, registerResponseFinalizers } from '../../../extensions';
|
|
10
|
+
import { getWorkUnit } from '../../../request/context';
|
|
11
|
+
import type { MetadataLink } from '../../../types';
|
|
12
|
+
import { persistFont, persistedFont } from './cache';
|
|
13
|
+
import {
|
|
14
|
+
familyStack,
|
|
15
|
+
fontStyle,
|
|
16
|
+
googleFontNaming,
|
|
17
|
+
localFontNaming,
|
|
18
|
+
singleQuoteFamily,
|
|
19
|
+
stableJson,
|
|
20
|
+
type FontDisplay,
|
|
21
|
+
type GoogleFontOptions,
|
|
22
|
+
type LocalFontOptions,
|
|
23
|
+
type LocalFontSource,
|
|
24
|
+
type NextFont,
|
|
25
|
+
type NextFontWithVariable,
|
|
26
|
+
} from './shared';
|
|
27
|
+
|
|
28
|
+
export type {
|
|
29
|
+
FontDisplay,
|
|
30
|
+
GoogleFontOptions,
|
|
31
|
+
LocalFontOptions,
|
|
32
|
+
LocalFontSource,
|
|
33
|
+
NextFont,
|
|
34
|
+
NextFontWithVariable,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
interface FontScope {
|
|
38
|
+
fonts: Map<string, FontDefinition>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface FontDefinition {
|
|
42
|
+
className: string;
|
|
43
|
+
family: string;
|
|
44
|
+
key: string;
|
|
45
|
+
resolve(context: FontResolveContext): Promise<ResolvedFont>;
|
|
46
|
+
style: NextFont['style'];
|
|
47
|
+
variable: string;
|
|
48
|
+
hasVariable: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface FontResolveContext {
|
|
52
|
+
config: ResolvedConfig;
|
|
53
|
+
dev?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ResolvedFontAssets {
|
|
57
|
+
css: string;
|
|
58
|
+
preloads: MetadataLink[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface ResolvedFont {
|
|
62
|
+
css: string;
|
|
63
|
+
/** Preloadable font links (empty when `preload:false`). Delivered via the
|
|
64
|
+
* `Link` response header in prod, not as HTML `<link>` tags. */
|
|
65
|
+
preloads: MetadataLink[];
|
|
66
|
+
/** Whether this definition emitted any font file (used to decide preconnect). */
|
|
67
|
+
usesFonts: boolean;
|
|
68
|
+
/** Emitted font files on disk, re-checked on every resolve — a rebuild can
|
|
69
|
+
* wipe the output directory under a running server. */
|
|
70
|
+
files: string[];
|
|
71
|
+
/** Local source files this resolution read; the persisted record stats them. */
|
|
72
|
+
sources?: string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface GoogleFontMetadata {
|
|
76
|
+
axes?: { tag: string; min: number; max: number; defaultValue?: number }[];
|
|
77
|
+
styles: string[];
|
|
78
|
+
subsets: string[];
|
|
79
|
+
weights: string[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface NormalizedGoogleOptions {
|
|
83
|
+
axes?: string[];
|
|
84
|
+
adjustFontFallback: boolean;
|
|
85
|
+
display: FontDisplay;
|
|
86
|
+
fallback: string[];
|
|
87
|
+
preload: boolean;
|
|
88
|
+
styles: string[];
|
|
89
|
+
subsets?: string[];
|
|
90
|
+
variable?: string;
|
|
91
|
+
weights: string[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface NormalizedLocalSource {
|
|
95
|
+
file: string;
|
|
96
|
+
style?: string;
|
|
97
|
+
weight?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const fontStorageKey = Symbol.for('pnext.compat.font.storage');
|
|
101
|
+
const fontRuntimeGlobal = globalThis as typeof globalThis & Record<symbol, AsyncLocalStorage<FontScope> | undefined>;
|
|
102
|
+
const fontStorage = fontRuntimeGlobal[fontStorageKey] ??= new AsyncLocalStorage<FontScope>();
|
|
103
|
+
const allowedDisplayValues = new Set(['auto', 'block', 'swap', 'fallback', 'optional']);
|
|
104
|
+
const require = createRequire(import.meta.url);
|
|
105
|
+
const googleCssCache = new Map<string, Promise<string>>();
|
|
106
|
+
const googleFontFileCache = new Map<string, Promise<Uint8Array>>();
|
|
107
|
+
const resolvedFontCache = new Map<string, Promise<ResolvedFont>>();
|
|
108
|
+
let googleMetadataCache: Record<string, GoogleFontMetadata> | undefined;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Every font definition constructed so far, whether or not a render has read its `.className` yet.
|
|
112
|
+
* Definitions are created when the app's font module evaluates - well before the render that registers
|
|
113
|
+
* them - which is what makes the resolve prewarmable.
|
|
114
|
+
*/
|
|
115
|
+
const declaredFonts = new Map<string, FontDefinition>();
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Resolve the declared fonts ahead of the render that will ask for them. Fire-and-forget: this only
|
|
119
|
+
* fills `resolvedFontCache`, so a failure costs nothing - the render's own `resolveFont` re-runs and
|
|
120
|
+
* surfaces the error.
|
|
121
|
+
*/
|
|
122
|
+
export async function prewarmFonts(
|
|
123
|
+
config: ResolvedConfig,
|
|
124
|
+
options: { dev?: boolean } = {},
|
|
125
|
+
): Promise<void> {
|
|
126
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
127
|
+
if (process.env.PNEXT_PREWARM_FONTS === '0' || declaredFonts.size === 0) return;
|
|
128
|
+
await Promise.allSettled(
|
|
129
|
+
[...declaredFonts.values()].map(font => resolveFont(font, { config, dev: options.dev })),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function runWithFontScope<T>(callback: () => T): T {
|
|
134
|
+
// Ensure the `Link`-header finalizer is registered before any render runs.
|
|
135
|
+
// Idempotent; register-font also calls it (canonical orchestrator seam).
|
|
136
|
+
registerFontFinalizerOnce();
|
|
137
|
+
return fontStorage.run({ fonts: new Map() }, callback);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Work-unit stash key for the font `Link` header preloads collected in a render. */
|
|
141
|
+
const FONT_HEADER_PRELOADS = Symbol.for('pnext.compat.font.headerPreloads');
|
|
142
|
+
|
|
143
|
+
export async function fontAssets(config: ResolvedConfig, options: { dev?: boolean } = {}): Promise<ResolvedFontAssets> {
|
|
144
|
+
const scope = fontStorage.getStore();
|
|
145
|
+
if (!scope || scope.fonts.size === 0) return { css: '', preloads: [] };
|
|
146
|
+
|
|
147
|
+
const resolved = await Promise.all(
|
|
148
|
+
[...scope.fonts.values()].map(font => resolveFont(font, { config, dev: options.dev })),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const preloads = dedupePreloads(resolved.flatMap(font => font.preloads));
|
|
152
|
+
// Next delivers font preloads via a `Link` response header (not HTML link
|
|
153
|
+
// tags). Stash them on the request work unit; the response finalizer
|
|
154
|
+
// (registerFontResponseFinalizer) reads them back and sets the header.
|
|
155
|
+
stashHeaderPreloads(preloads);
|
|
156
|
+
|
|
157
|
+
const usesFonts = resolved.some(font => font.usesFonts);
|
|
158
|
+
return {
|
|
159
|
+
css: resolved.map(font => font.css).filter(Boolean).join('\n'),
|
|
160
|
+
// Prod matches Next: font preloads travel EXCLUSIVELY in the `Link` response header, so the SSR
|
|
161
|
+
// document carries no `<link as="font">` tags. The live DOM still gets the links post-load via
|
|
162
|
+
// fontPreloadDomScript, which derives them from the emitted @font-face CSS - Next's client inserts
|
|
163
|
+
// them the same way, and the navigation suite counts them in the browser. Dev keeps the document
|
|
164
|
+
// links, since it has no header path.
|
|
165
|
+
preloads:
|
|
166
|
+
preloads.length > 0
|
|
167
|
+
? options.dev
|
|
168
|
+
? preloads
|
|
169
|
+
: []
|
|
170
|
+
: usesFonts
|
|
171
|
+
? [fontPreconnect()]
|
|
172
|
+
: [],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Inline script that mirrors Next's client-side font preload insertion: scan
|
|
178
|
+
* same-origin stylesheets for @font-face sources whose filename carries the
|
|
179
|
+
* `.p.` preload marker and insert a `<link rel="preload" as="font">` for each
|
|
180
|
+
* (deduped against any existing link). Prod-only companion to the header-only
|
|
181
|
+
* delivery above.
|
|
182
|
+
*/
|
|
183
|
+
export function fontPreloadDomScript(): string {
|
|
184
|
+
return (
|
|
185
|
+
'<script>(function(){try{function a(){var seen={},ls=document.querySelectorAll(\'link[as="font"]\');' +
|
|
186
|
+
'for(var i=0;i<ls.length;i++)seen[ls[i].href]=1;' +
|
|
187
|
+
'for(var i=0;i<document.styleSheets.length;i++){var rules;try{rules=document.styleSheets[i].cssRules}catch(e){continue}if(!rules)continue;' +
|
|
188
|
+
'for(var j=0;j<rules.length;j++){var r=rules[j];if(!(r.type===5))continue;' +
|
|
189
|
+
'var m=/url\\((?:"|\')?([^"\')\\s]*\\.p\\.woff2)/.exec(r.cssText);if(!m)continue;' +
|
|
190
|
+
'var u;try{u=new URL(m[1],location.href).href}catch(e){continue}if(seen[u])continue;seen[u]=1;' +
|
|
191
|
+
'var l=document.createElement("link");l.rel="preload";l.as="font";l.type="font/woff2";l.setAttribute("crossorigin","");l.href=u;document.head.appendChild(l)}}}' +
|
|
192
|
+
'document.readyState==="loading"?document.addEventListener("DOMContentLoaded",a):a()}catch(e){}})();</script>'
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function stashHeaderPreloads(preloads: MetadataLink[]): void {
|
|
197
|
+
if (preloads.length === 0) return;
|
|
198
|
+
const unit = getWorkUnit();
|
|
199
|
+
if (!unit) return;
|
|
200
|
+
const compat = (unit.compat ??= {});
|
|
201
|
+
const existing = (compat[FONT_HEADER_PRELOADS] as MetadataLink[] | undefined) ?? [];
|
|
202
|
+
compat[FONT_HEADER_PRELOADS] = dedupePreloads([...existing, ...preloads]);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Read the font `Link` header preloads stashed during the current render. */
|
|
206
|
+
export function takeFontHeaderPreloads(): MetadataLink[] {
|
|
207
|
+
const unit = getWorkUnit();
|
|
208
|
+
const compat = unit?.compat;
|
|
209
|
+
if (!compat) return [];
|
|
210
|
+
const preloads = (compat[FONT_HEADER_PRELOADS] as MetadataLink[] | undefined) ?? [];
|
|
211
|
+
compat[FONT_HEADER_PRELOADS] = undefined;
|
|
212
|
+
return preloads;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let fontFinalizerRegistered = false;
|
|
216
|
+
// Host-scoped guard: the finalizer lives on the host, so a fresh host needs it again.
|
|
217
|
+
onExtensionHostReset(() => (fontFinalizerRegistered = false));
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Register the response finalizer that flushes the stashed font preloads to the
|
|
221
|
+
* `Link` header. Idempotent: safe to call from both register-font (the canonical
|
|
222
|
+
* orchestrator seam) and lazily from font-scope setup, so preloads reach the
|
|
223
|
+
* header even before the orchestrator wires register-font in.
|
|
224
|
+
*/
|
|
225
|
+
export function registerFontFinalizerOnce(): void {
|
|
226
|
+
if (fontFinalizerRegistered) return;
|
|
227
|
+
fontFinalizerRegistered = true;
|
|
228
|
+
registerResponseFinalizers(ctx => {
|
|
229
|
+
const preloads = takeFontHeaderPreloads();
|
|
230
|
+
if (preloads.length === 0) return;
|
|
231
|
+
const value = preloads.map(fontLinkHeaderValue).join(', ');
|
|
232
|
+
const existing = ctx.headers.get('link');
|
|
233
|
+
ctx.headers.set('link', existing ? `${existing}, ${value}` : value);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** `<url>; rel=preload; as=font; type=font/woff2; crossorigin=""` */
|
|
238
|
+
export function fontLinkHeaderValue(link: MetadataLink): string {
|
|
239
|
+
const parts = [`<${link.url}>`, `rel=${link.rel}`];
|
|
240
|
+
if (link.as) parts.push(`as=${link.as}`);
|
|
241
|
+
if (link.type) parts.push(`type=${link.type}`);
|
|
242
|
+
parts.push('crossorigin=""');
|
|
243
|
+
return parts.join('; ');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function googleFont(family: string) {
|
|
247
|
+
return (options: GoogleFontOptions = {}): NextFontWithVariable => {
|
|
248
|
+
const naming = googleFontNaming(family, options);
|
|
249
|
+
const { className, variable, fontFamily, hash, style, hasVariable } = naming;
|
|
250
|
+
const definition: FontDefinition = {
|
|
251
|
+
className,
|
|
252
|
+
family: fontFamily,
|
|
253
|
+
key: `google:${stableJson({ family, options })}`,
|
|
254
|
+
style,
|
|
255
|
+
variable,
|
|
256
|
+
hasVariable,
|
|
257
|
+
resolve: context => resolveGoogleFont(context, {
|
|
258
|
+
className,
|
|
259
|
+
family,
|
|
260
|
+
fontFamily,
|
|
261
|
+
hash,
|
|
262
|
+
options,
|
|
263
|
+
variable,
|
|
264
|
+
}),
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
return fontObject(definition);
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export interface LocalFontLoader {
|
|
272
|
+
(options: LocalFontOptions): NextFontWithVariable;
|
|
273
|
+
withFile: typeof localFontWithFile;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export const localFont: LocalFontLoader = Object.assign(
|
|
277
|
+
(options: LocalFontOptions) => localFontWithFile(inferLocalFontCallerFile())(options),
|
|
278
|
+
{ withFile: localFontWithFile },
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
export function localFontWithFile(callerFile: string | undefined, family?: string) {
|
|
282
|
+
return (options: LocalFontOptions): NextFontWithVariable => {
|
|
283
|
+
const naming = localFontNaming(callerFile, options);
|
|
284
|
+
if (family) {
|
|
285
|
+
naming.fontFamily = family;
|
|
286
|
+
naming.style = fontStyle(family, options, options.fallback ?? []);
|
|
287
|
+
}
|
|
288
|
+
const { className, variable, fontFamily, hash, style, hasVariable } = naming;
|
|
289
|
+
const definition: FontDefinition = {
|
|
290
|
+
className,
|
|
291
|
+
family: fontFamily,
|
|
292
|
+
key: `local:${stableJson({ callerFile, options })}`,
|
|
293
|
+
style,
|
|
294
|
+
variable,
|
|
295
|
+
hasVariable,
|
|
296
|
+
resolve: context => resolveLocalFont(context, {
|
|
297
|
+
callerFile,
|
|
298
|
+
className,
|
|
299
|
+
fontFamily,
|
|
300
|
+
hash,
|
|
301
|
+
options,
|
|
302
|
+
variable,
|
|
303
|
+
}),
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
return fontObject(definition);
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function fontObject(definition: FontDefinition): NextFontWithVariable {
|
|
311
|
+
// Recorded at construction, not at first `.className` read: the whole point
|
|
312
|
+
// is to know the font exists before any render registers it.
|
|
313
|
+
declaredFonts.set(definition.key, definition);
|
|
314
|
+
// `variable` is exposed only when the loader call passed a `variable` option
|
|
315
|
+
// (matches Next; an extra key would break `toEqual` on the font object).
|
|
316
|
+
const descriptors: PropertyDescriptorMap = {
|
|
317
|
+
className: {
|
|
318
|
+
enumerable: true,
|
|
319
|
+
get() {
|
|
320
|
+
registerFont(definition);
|
|
321
|
+
return definition.className;
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
style: {
|
|
325
|
+
enumerable: true,
|
|
326
|
+
get() {
|
|
327
|
+
registerFont(definition);
|
|
328
|
+
return definition.style;
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
if (definition.hasVariable) {
|
|
333
|
+
descriptors.variable = {
|
|
334
|
+
enumerable: true,
|
|
335
|
+
get() {
|
|
336
|
+
registerFont(definition);
|
|
337
|
+
return definition.variable;
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
return Object.defineProperties({}, descriptors) as NextFontWithVariable;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function registerFont(definition: FontDefinition) {
|
|
345
|
+
fontStorage.getStore()?.fonts.set(definition.key, definition);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function resolveFont(definition: FontDefinition, context: FontResolveContext) {
|
|
349
|
+
const key = `${context.config.outPath}:${context.dev ? 'dev' : 'build'}:${definition.key}`;
|
|
350
|
+
let resolved = resolvedFontCache.get(key);
|
|
351
|
+
if (!resolved) {
|
|
352
|
+
resolved = resolveThroughCache(definition, context, key);
|
|
353
|
+
resolvedFontCache.set(key, resolved);
|
|
354
|
+
}
|
|
355
|
+
const font = await resolved;
|
|
356
|
+
// A `pnext build` wiping the output directory under a running server must
|
|
357
|
+
// not leave cached font URLs pointing at deleted files; re-emit them.
|
|
358
|
+
// Font bytes stay cached in memory, so this never refetches the network.
|
|
359
|
+
if (font.files.every(file => existsSync(file))) return font;
|
|
360
|
+
const fresh = resolveAndPersist(definition, context, key);
|
|
361
|
+
resolvedFontCache.set(key, fresh);
|
|
362
|
+
return fresh;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Dev reuses the persisted resolution when it still describes disk: a restart otherwise re-fetches every
|
|
367
|
+
* Google stylesheet and font file to re-derive URLs whose bytes are already emitted. A build always
|
|
368
|
+
* resolves for real - it owns the output directory it just emptied.
|
|
369
|
+
*/
|
|
370
|
+
async function resolveThroughCache(
|
|
371
|
+
definition: FontDefinition,
|
|
372
|
+
context: FontResolveContext,
|
|
373
|
+
key: string,
|
|
374
|
+
): Promise<ResolvedFont> {
|
|
375
|
+
if (!context.dev) return definition.resolve(context);
|
|
376
|
+
const persisted = await persistedFont(context.config, key);
|
|
377
|
+
if (persisted) return persisted;
|
|
378
|
+
return resolveAndPersist(definition, context, key);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function resolveAndPersist(
|
|
382
|
+
definition: FontDefinition,
|
|
383
|
+
context: FontResolveContext,
|
|
384
|
+
key: string,
|
|
385
|
+
): Promise<ResolvedFont> {
|
|
386
|
+
const resolved = definition.resolve(context);
|
|
387
|
+
if (context.dev) {
|
|
388
|
+
void resolved.then(font => persistFont(context.config, key, font)).catch(() => undefined);
|
|
389
|
+
}
|
|
390
|
+
return resolved;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function resolveGoogleFont(
|
|
394
|
+
context: FontResolveContext,
|
|
395
|
+
request: {
|
|
396
|
+
className: string;
|
|
397
|
+
family: string;
|
|
398
|
+
fontFamily: string;
|
|
399
|
+
hash: string;
|
|
400
|
+
options: GoogleFontOptions;
|
|
401
|
+
variable: string;
|
|
402
|
+
},
|
|
403
|
+
): Promise<ResolvedFont> {
|
|
404
|
+
const metadata = googleMetadata()[request.family];
|
|
405
|
+
if (!metadata) throw new Error(`Unknown font \`${request.family}\``);
|
|
406
|
+
const options = normalizeGoogleOptions(request.family, metadata, request.options);
|
|
407
|
+
const adjustFontFallback = options.adjustFontFallback;
|
|
408
|
+
const url = googleFontsUrl(request.family, googleFontAxes(metadata, options), options.display);
|
|
409
|
+
const css = (await googleCss(url, request.family, Boolean(context.dev))).split('body {', 1)[0] ?? '';
|
|
410
|
+
const fontFiles = findGoogleFontFiles(css, options.preload ? options.subsets : undefined);
|
|
411
|
+
const emitted = await Promise.all(
|
|
412
|
+
fontFiles.map(async fontFile => ({
|
|
413
|
+
...fontFile,
|
|
414
|
+
emitted: await emitRemoteFontFile(context, fontFile.googleFontFileUrl, {
|
|
415
|
+
preload: fontFile.preloadFontFile,
|
|
416
|
+
adjustFontFallback,
|
|
417
|
+
}),
|
|
418
|
+
})),
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
let rewrittenCss = css;
|
|
422
|
+
for (const item of emitted) {
|
|
423
|
+
rewrittenCss = rewrittenCss.replaceAll(item.googleFontFileUrl, item.emitted.url);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const stack = familyStack(request.fontFamily, options.fallback, adjustFontFallback);
|
|
427
|
+
const scopedCss = [
|
|
428
|
+
rewriteFontFamily(rewrittenCss, request.family, request.fontFamily),
|
|
429
|
+
adjustFontFallback ? fallbackFaceCss(request.fontFamily) : '',
|
|
430
|
+
classRule(request.className, stack, singleCssValue(options.weights), singleCssValue(options.styles)),
|
|
431
|
+
request.options.variable ? variableRule(request.variable, customProperty(request.options.variable), stack) : '',
|
|
432
|
+
].filter(Boolean).join('\n');
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
usesFonts: emitted.length > 0,
|
|
436
|
+
css: scopedCss,
|
|
437
|
+
preloads: emitted
|
|
438
|
+
.filter(item => item.preloadFontFile)
|
|
439
|
+
.map(item => fontPreload(item.emitted.url, item.emitted.type)),
|
|
440
|
+
files: emitted.map(item => item.emitted.file),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function resolveLocalFont(
|
|
445
|
+
context: FontResolveContext,
|
|
446
|
+
request: {
|
|
447
|
+
callerFile: string | undefined;
|
|
448
|
+
className: string;
|
|
449
|
+
fontFamily: string;
|
|
450
|
+
hash: string;
|
|
451
|
+
options: LocalFontOptions;
|
|
452
|
+
variable: string;
|
|
453
|
+
},
|
|
454
|
+
): Promise<ResolvedFont> {
|
|
455
|
+
const display = validateDisplay(request.options.display ?? 'swap');
|
|
456
|
+
const adjustFontFallback = request.options.declarations?.some(item => item.prop === 'font-family')
|
|
457
|
+
? false
|
|
458
|
+
: true;
|
|
459
|
+
const preload = request.options.preload !== false;
|
|
460
|
+
const sources = normalizeLocalSources(request.options, request.callerFile);
|
|
461
|
+
const emitted = await Promise.all(sources.map(async source => ({
|
|
462
|
+
...source,
|
|
463
|
+
emitted: await emitLocalFontFile(context, source.file, { preload, adjustFontFallback }),
|
|
464
|
+
})));
|
|
465
|
+
const stack = familyStack(request.fontFamily, request.options.fallback ?? [], adjustFontFallback);
|
|
466
|
+
const declarations = request.options.declarations ?? [];
|
|
467
|
+
const faceCss = emitted.map(source =>
|
|
468
|
+
[
|
|
469
|
+
'@font-face{',
|
|
470
|
+
`font-family:${singleQuoteFamily(request.fontFamily)};`,
|
|
471
|
+
`src:url("${source.emitted.url}") format("${fontFormat(source.emitted.ext)}");`,
|
|
472
|
+
`font-display:${display};`,
|
|
473
|
+
source.weight ?? request.options.weight ? `font-weight:${cssIdentifier(source.weight ?? request.options.weight ?? '')};` : '',
|
|
474
|
+
source.style ?? request.options.style ? `font-style:${cssIdentifier(source.style ?? request.options.style ?? '')};` : '',
|
|
475
|
+
...declarations.map(item => `${cssProperty(item.prop)}:${cssDeclarationValue(item.value)};`),
|
|
476
|
+
'}',
|
|
477
|
+
].filter(Boolean).join(''),
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
usesFonts: emitted.length > 0,
|
|
482
|
+
css: [
|
|
483
|
+
...faceCss,
|
|
484
|
+
adjustFontFallback ? fallbackFaceCss(request.fontFamily) : '',
|
|
485
|
+
classRule(request.className, stack, request.options.weight, request.options.style),
|
|
486
|
+
request.options.variable ? variableRule(request.variable, customProperty(request.options.variable), stack) : '',
|
|
487
|
+
].filter(Boolean).join('\n'),
|
|
488
|
+
preloads: preload
|
|
489
|
+
? emitted.map(item => fontPreload(item.emitted.url, item.emitted.type))
|
|
490
|
+
: [],
|
|
491
|
+
files: emitted.map(item => item.emitted.file),
|
|
492
|
+
sources: emitted.map(source => source.file),
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function normalizeGoogleOptions(
|
|
497
|
+
family: string,
|
|
498
|
+
metadata: GoogleFontMetadata,
|
|
499
|
+
options: GoogleFontOptions,
|
|
500
|
+
): NormalizedGoogleOptions {
|
|
501
|
+
const display = validateDisplay(options.display ?? 'swap');
|
|
502
|
+
const preload = options.preload ?? true;
|
|
503
|
+
const subsets = options.subsets;
|
|
504
|
+
if (metadata.subsets.length === 0) {
|
|
505
|
+
// Matches Next: fonts without preloadable subsets silently disable preload.
|
|
506
|
+
} else if (preload) {
|
|
507
|
+
if (!subsets) {
|
|
508
|
+
throw new Error(
|
|
509
|
+
`Preload is enabled but no subsets were specified for font \`${family}\`. ` +
|
|
510
|
+
`Please specify subsets or disable preloading.`,
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
for (const subset of subsets) {
|
|
514
|
+
if (!metadata.subsets.includes(subset)) {
|
|
515
|
+
throw new Error(`Unknown subset \`${subset}\` for font \`${family}\`.`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const weights = uniqueArray(options.weight ? arrayValue(options.weight) : []);
|
|
521
|
+
if (weights.length === 0) {
|
|
522
|
+
if (metadata.weights.includes('variable')) weights.push('variable');
|
|
523
|
+
else throw new Error(`Missing weight for font \`${family}\`.`);
|
|
524
|
+
}
|
|
525
|
+
if (weights.length > 1 && weights.includes('variable')) {
|
|
526
|
+
throw new Error(`Unexpected \`variable\` in weight array for font \`${family}\`.`);
|
|
527
|
+
}
|
|
528
|
+
for (const weight of weights) {
|
|
529
|
+
if (!metadata.weights.includes(weight)) throw new Error(`Unknown weight \`${weight}\` for font \`${family}\`.`);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const styles = uniqueArray(options.style ? arrayValue(options.style) : []);
|
|
533
|
+
if (styles.length === 0) styles.push(metadata.styles.length === 1 ? (metadata.styles[0] ?? 'normal') : 'normal');
|
|
534
|
+
for (const style of styles) {
|
|
535
|
+
if (!metadata.styles.includes(style)) throw new Error(`Unknown style \`${style}\` for font \`${family}\`.`);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (options.axes) {
|
|
539
|
+
if (!metadata.weights.includes('variable') || weights[0] !== 'variable') {
|
|
540
|
+
throw new Error('Axes can only be defined for variable fonts.');
|
|
541
|
+
}
|
|
542
|
+
const definableAxes = (metadata.axes ?? []).map(axis => axis.tag).filter(tag => tag !== 'wght');
|
|
543
|
+
for (const axis of options.axes) {
|
|
544
|
+
if (!definableAxes.includes(axis)) throw new Error(`Invalid axes value \`${axis}\` for font \`${family}\`.`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return {
|
|
549
|
+
axes: options.axes,
|
|
550
|
+
adjustFontFallback: options.adjustFontFallback ?? true,
|
|
551
|
+
display,
|
|
552
|
+
fallback: options.fallback ?? [],
|
|
553
|
+
preload: metadata.subsets.length === 0 ? false : preload,
|
|
554
|
+
styles,
|
|
555
|
+
subsets,
|
|
556
|
+
variable: options.variable,
|
|
557
|
+
weights,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function googleFontAxes(metadata: GoogleFontMetadata, options: NormalizedGoogleOptions) {
|
|
562
|
+
const hasItalic = options.styles.includes('italic');
|
|
563
|
+
const hasNormal = options.styles.includes('normal');
|
|
564
|
+
const ital = hasItalic ? [...(hasNormal ? ['0'] : []), '1'] : undefined;
|
|
565
|
+
|
|
566
|
+
if (options.weights[0] !== 'variable') {
|
|
567
|
+
return { ital, wght: options.weights, variableAxes: undefined as [string, string][] | undefined };
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
let weightAxis: string | undefined;
|
|
571
|
+
const variableAxes: [string, string][] = [];
|
|
572
|
+
for (const axis of metadata.axes ?? []) {
|
|
573
|
+
if (axis.tag === 'wght') weightAxis = `${axis.min}..${axis.max}`;
|
|
574
|
+
else if (options.axes?.includes(axis.tag)) variableAxes.push([axis.tag, `${axis.min}..${axis.max}`]);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return {
|
|
578
|
+
ital,
|
|
579
|
+
wght: weightAxis ? [weightAxis] : undefined,
|
|
580
|
+
variableAxes: variableAxes.length > 0 ? variableAxes : undefined,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function googleFontsUrl(
|
|
585
|
+
family: string,
|
|
586
|
+
axes: { ital?: string[]; wght?: string[]; variableAxes?: [string, string][] },
|
|
587
|
+
display: FontDisplay,
|
|
588
|
+
) {
|
|
589
|
+
const variants: [string, string][][] = [];
|
|
590
|
+
if (axes.wght) {
|
|
591
|
+
for (const wght of axes.wght) {
|
|
592
|
+
if (!axes.ital) {
|
|
593
|
+
variants.push([['wght', wght], ...(axes.variableAxes ?? [])]);
|
|
594
|
+
} else {
|
|
595
|
+
for (const ital of axes.ital) variants.push([['ital', ital], ['wght', wght], ...(axes.variableAxes ?? [])]);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
} else if (axes.variableAxes) {
|
|
599
|
+
variants.push([...axes.variableAxes]);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (axes.variableAxes) {
|
|
603
|
+
for (const variant of variants) {
|
|
604
|
+
variant.sort(([a], [b]) => {
|
|
605
|
+
const aLower = a.charCodeAt(0) > 96;
|
|
606
|
+
const bLower = b.charCodeAt(0) > 96;
|
|
607
|
+
if (aLower && !bLower) return -1;
|
|
608
|
+
if (bLower && !aLower) return 1;
|
|
609
|
+
return a > b ? 1 : -1;
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
let url = `https://fonts.googleapis.com/css2?family=${family.replace(/ /g, '+')}`;
|
|
615
|
+
const firstVariant = variants[0];
|
|
616
|
+
if (firstVariant) {
|
|
617
|
+
url += `:${firstVariant.map(([key]) => key).join(',')}@${variants
|
|
618
|
+
.map(variant => variant.map(([, value]) => value).join(','))
|
|
619
|
+
.sort(sortFontVariantValues)
|
|
620
|
+
.join(';')}`;
|
|
621
|
+
}
|
|
622
|
+
return `${url}&display=${display}`;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async function googleCss(url: string, family: string, dev: boolean) {
|
|
626
|
+
let cached = googleCssCache.get(url);
|
|
627
|
+
if (!cached) {
|
|
628
|
+
cached = fetchGoogleCss(url, family, dev);
|
|
629
|
+
googleCssCache.set(url, cached);
|
|
630
|
+
}
|
|
631
|
+
return cached;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async function fetchGoogleCss(url: string, family: string, dev: boolean) {
|
|
635
|
+
const mocked = mockedGoogleResponse(url);
|
|
636
|
+
if (typeof mocked === 'string') return mocked;
|
|
637
|
+
|
|
638
|
+
const response = await fetch(url, {
|
|
639
|
+
headers: {
|
|
640
|
+
// Google returns modern woff2 CSS for current browsers.
|
|
641
|
+
'user-agent': 'Mozilla/5.0 AppleWebKit/537.36 Chrome/120 Safari/537.36',
|
|
642
|
+
},
|
|
643
|
+
});
|
|
644
|
+
if (!response.ok) {
|
|
645
|
+
const message = `Failed to fetch font \`${family}\` from Google Fonts: ${response.status} ${response.statusText}`;
|
|
646
|
+
throw new Error(dev ? `${message}. Use NEXT_FONT_GOOGLE_MOCKED_RESPONSES for offline tests.` : message);
|
|
647
|
+
}
|
|
648
|
+
return response.text();
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function findGoogleFontFiles(css: string, subsetsToPreload: string[] | undefined) {
|
|
652
|
+
const fontFiles: { googleFontFileUrl: string; preloadFontFile: boolean }[] = [];
|
|
653
|
+
let currentSubset = '';
|
|
654
|
+
for (const line of css.split('\n')) {
|
|
655
|
+
const subset = /\/\* (.+?) \*\//.exec(line)?.[1];
|
|
656
|
+
if (subset) {
|
|
657
|
+
currentSubset = subset;
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const googleFontFileUrl = /src: url\((.+?)\)/.exec(line)?.[1];
|
|
662
|
+
if (!googleFontFileUrl || fontFiles.some(file => file.googleFontFileUrl === googleFontFileUrl)) continue;
|
|
663
|
+
fontFiles.push({
|
|
664
|
+
googleFontFileUrl,
|
|
665
|
+
preloadFontFile: Boolean(subsetsToPreload?.includes(currentSubset)),
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
return fontFiles;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
interface EmitFontOptions {
|
|
672
|
+
preload: boolean;
|
|
673
|
+
adjustFontFallback: boolean;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async function emitRemoteFontFile(context: FontResolveContext, url: string, emit: EmitFontOptions) {
|
|
677
|
+
let cached = googleFontFileCache.get(url);
|
|
678
|
+
if (!cached) {
|
|
679
|
+
cached = fetchFontFile(url);
|
|
680
|
+
googleFontFileCache.set(url, cached);
|
|
681
|
+
}
|
|
682
|
+
const bytes = await cached;
|
|
683
|
+
return emitFontBytes(context, bytes, fontExt(url), emit);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async function fetchFontFile(url: string) {
|
|
687
|
+
const mocked = mockedGoogleResponse(url);
|
|
688
|
+
if (mocked instanceof Uint8Array) return mocked;
|
|
689
|
+
if (typeof mocked === 'string') return new TextEncoder().encode(mocked);
|
|
690
|
+
|
|
691
|
+
const response = await fetch(url);
|
|
692
|
+
if (!response.ok) throw new Error(`Failed to fetch font file from \`${url}\`.`);
|
|
693
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
async function emitLocalFontFile(context: FontResolveContext, file: string, emit: EmitFontOptions) {
|
|
697
|
+
const bytes = await readFile(file);
|
|
698
|
+
return emitFontBytes(context, bytes, fontExt(file), emit, file);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
async function emitFontBytes(
|
|
702
|
+
context: FontResolveContext,
|
|
703
|
+
bytes: Uint8Array,
|
|
704
|
+
ext: string,
|
|
705
|
+
emit: EmitFontOptions,
|
|
706
|
+
sourceFile?: string,
|
|
707
|
+
) {
|
|
708
|
+
const hash = createHash('sha256').update(bytes).digest('hex').slice(0, 16);
|
|
709
|
+
// Next's loader names emitted files `[hash]-s.p.[ext]`: `-s` when a
|
|
710
|
+
// size-adjust fallback font is used, `.p` when the font is preloaded.
|
|
711
|
+
const filename = `${hash}${emit.adjustFontFallback ? '-s' : ''}${emit.preload ? '.p' : ''}.${ext}`;
|
|
712
|
+
// Served from `/_next/static/media/` (files under public/ map 1:1 to the URL
|
|
713
|
+
// path). Dev stashes under cache/ but keeps the same relative layout.
|
|
714
|
+
const mediaSegments = ['_next', 'static', 'media'];
|
|
715
|
+
const outRoot = context.dev
|
|
716
|
+
? path.join(context.config.outPath, 'cache')
|
|
717
|
+
: path.join(context.config.outPath, 'public');
|
|
718
|
+
const outDir = path.join(outRoot, ...mediaSegments);
|
|
719
|
+
const file = path.join(outDir, filename);
|
|
720
|
+
await mkdir(outDir, { recursive: true });
|
|
721
|
+
if (!existsSync(file)) {
|
|
722
|
+
if (sourceFile) await copyFile(sourceFile, file);
|
|
723
|
+
else await writeFile(file, bytes);
|
|
724
|
+
}
|
|
725
|
+
return { ext, file, type: fontMimeType(ext), url: `/${mediaSegments.join('/')}/${filename}` };
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function normalizeLocalSources(options: LocalFontOptions, callerFile: string | undefined): NormalizedLocalSource[] {
|
|
729
|
+
if (!callerFile) throw new Error('next/font/local requires PNext server source transforms.');
|
|
730
|
+
const values = Array.isArray(options.src) ? options.src : [options.src];
|
|
731
|
+
return values.map(value => {
|
|
732
|
+
const item = typeof value === 'string' ? { path: value } : value;
|
|
733
|
+
if (!item.path) throw new Error('next/font/local src entries require a path.');
|
|
734
|
+
const file = path.resolve(path.dirname(callerFile), item.path);
|
|
735
|
+
if (!existsSync(file)) throw new Error(`Font file not found: ${item.path}`);
|
|
736
|
+
return { file, style: item.style, weight: item.weight };
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function inferLocalFontCallerFile(): string | undefined {
|
|
741
|
+
const stack = new Error().stack;
|
|
742
|
+
if (!stack) return undefined;
|
|
743
|
+
for (const line of stack.split('\n')) {
|
|
744
|
+
const file = stackFrameFile(line);
|
|
745
|
+
if (!file) continue;
|
|
746
|
+
if (file.replace(/\\/g, '/').includes('/src/compat/next/font/')) continue;
|
|
747
|
+
return originalSourceFile(file);
|
|
748
|
+
}
|
|
749
|
+
return undefined;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function stackFrameFile(line: string) {
|
|
753
|
+
const match = /\(?((?:file:\/\/)?\/[^():]+\.[cm]?[jt]sx?|(?:file:\/\/)?\/[^():]+\.mjs):\d+:\d+\)?/.exec(line);
|
|
754
|
+
const file = match?.[1];
|
|
755
|
+
if (!file) return undefined;
|
|
756
|
+
return file.startsWith('file://') ? fileURLToPath(file) : file;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function originalSourceFile(file: string) {
|
|
760
|
+
const source = sourceComment(file);
|
|
761
|
+
if (!source) return file;
|
|
762
|
+
const normalized = source.replace(/\\/g, '/');
|
|
763
|
+
if (normalized.startsWith('file://')) return fileURLToPath(normalized);
|
|
764
|
+
if (path.isAbsolute(normalized)) return existsSync(normalized) ? normalized : file;
|
|
765
|
+
const nodeModulesIndex = normalized.indexOf('node_modules/');
|
|
766
|
+
const relative = nodeModulesIndex === -1 ? normalized : normalized.slice(nodeModulesIndex);
|
|
767
|
+
const candidate = path.resolve(compiledProjectRoot(file), relative);
|
|
768
|
+
return existsSync(candidate) ? candidate : file;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function sourceComment(file: string) {
|
|
772
|
+
if (!existsSync(file)) return undefined;
|
|
773
|
+
const firstLine = readFileSync(file, 'utf8').split(/\r?\n/, 1)[0]?.trim();
|
|
774
|
+
return /^\/\/\s+(.+)$/.exec(firstLine ?? '')?.[1];
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function compiledProjectRoot(file: string) {
|
|
778
|
+
const marker = `${path.sep}.pnext${path.sep}`;
|
|
779
|
+
const index = file.indexOf(marker);
|
|
780
|
+
return index === -1 ? process.cwd() : file.slice(0, index);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function googleMetadata() {
|
|
784
|
+
if (!googleMetadataCache) {
|
|
785
|
+
googleMetadataCache = loadGoogleMetadata();
|
|
786
|
+
}
|
|
787
|
+
return googleMetadataCache;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function loadGoogleMetadata(): Record<string, GoogleFontMetadata> {
|
|
791
|
+
const override = process.env.PNEXT_FONT_GOOGLE_METADATA;
|
|
792
|
+
if (override) {
|
|
793
|
+
const metadata = readGoogleMetadataFile(override);
|
|
794
|
+
if (metadata) return metadata;
|
|
795
|
+
throw new Error(`Invalid Google font metadata file: ${override}`);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const metadata =
|
|
799
|
+
tryLoadGoogleMetadata('next-font/google/font-data.json') ??
|
|
800
|
+
tryLoadGoogleMetadata('next-font');
|
|
801
|
+
if (metadata) return metadata;
|
|
802
|
+
|
|
803
|
+
throw new Error(
|
|
804
|
+
'next/font/google requires Google font metadata. Install `next-font`.',
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function tryLoadGoogleMetadata(specifier: string): Record<string, GoogleFontMetadata> | undefined {
|
|
809
|
+
try {
|
|
810
|
+
const loaded = require(specifier) as {
|
|
811
|
+
default?: unknown;
|
|
812
|
+
googleFontsMetadata?: unknown;
|
|
813
|
+
} | Record<string, GoogleFontMetadata>;
|
|
814
|
+
const metadata =
|
|
815
|
+
'googleFontsMetadata' in loaded
|
|
816
|
+
? loaded.googleFontsMetadata
|
|
817
|
+
: 'default' in loaded
|
|
818
|
+
? loaded.default
|
|
819
|
+
: loaded;
|
|
820
|
+
return isGoogleMetadata(metadata) ? metadata : undefined;
|
|
821
|
+
} catch {
|
|
822
|
+
return undefined;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function readGoogleMetadataFile(file: string): Record<string, GoogleFontMetadata> | undefined {
|
|
827
|
+
const metadata: unknown = JSON.parse(readFileSync(file, 'utf8'));
|
|
828
|
+
return isGoogleMetadata(metadata) ? metadata : undefined;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function isGoogleMetadata(value: unknown): value is Record<string, GoogleFontMetadata> {
|
|
832
|
+
if (!value || typeof value !== 'object') return false;
|
|
833
|
+
const inter = (value as Record<string, GoogleFontMetadata>).Inter;
|
|
834
|
+
return Boolean(
|
|
835
|
+
inter &&
|
|
836
|
+
Array.isArray(inter.weights) &&
|
|
837
|
+
Array.isArray(inter.styles) &&
|
|
838
|
+
Array.isArray(inter.subsets),
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function mockedGoogleResponse(url: string) {
|
|
843
|
+
const mockFile = process.env.NEXT_FONT_GOOGLE_MOCKED_RESPONSES;
|
|
844
|
+
if (!mockFile) return undefined;
|
|
845
|
+
const mocked = require(mockFile) as Record<string, string>;
|
|
846
|
+
const value = mocked[url];
|
|
847
|
+
if (value === undefined) throw new Error(`Missing mocked response for URL: ${url}`);
|
|
848
|
+
if (typeof value === 'string' && value.startsWith('/') && existsSync(value)) {
|
|
849
|
+
return new Uint8Array(readFileSync(value));
|
|
850
|
+
}
|
|
851
|
+
return value;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function rewriteFontFamily(css: string, originalFamily: string, scopedFamily: string) {
|
|
855
|
+
const familyPattern = escapeRegex(originalFamily);
|
|
856
|
+
return css.replace(
|
|
857
|
+
new RegExp(`font-family:\\s*(["']?)${familyPattern}\\1`, 'g'),
|
|
858
|
+
`font-family:${singleQuoteFamily(scopedFamily)}`,
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/** Metrics-adjusted fallback `@font-face` (`'<family> Fallback'`). We omit the
|
|
863
|
+
* ascent/descent/size-adjust metrics (would need fontkit); the local() source
|
|
864
|
+
* never loads, so the browser resolves the real font or the next stack entry. */
|
|
865
|
+
function fallbackFaceCss(family: string) {
|
|
866
|
+
return `@font-face{font-family:${singleQuoteFamily(`${family} Fallback`)};src:local("Arial");}`;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function classRule(className: string, familyStack: string, weight?: string, style?: string) {
|
|
870
|
+
return [
|
|
871
|
+
`.${className}{font-family:${familyStack};`,
|
|
872
|
+
weight && !isRangeValue(weight) ? `font-weight:${cssIdentifier(weight)};` : '',
|
|
873
|
+
style && !isRangeValue(style) ? `font-style:${cssIdentifier(style)};` : '',
|
|
874
|
+
'}',
|
|
875
|
+
].join('');
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function variableRule(className: string, variable: string, familyStack: string) {
|
|
879
|
+
return `.${className}{${variable}:${familyStack};}`;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function fontPreload(url: string, type: string): MetadataLink {
|
|
883
|
+
return { rel: 'preload', url, as: 'font', type, crossOrigin: '' };
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/** Single `<link rel="preconnect" href="/" crossorigin="">` for the
|
|
887
|
+
* `preload:false` (no preloaded fonts) case. */
|
|
888
|
+
function fontPreconnect(): MetadataLink {
|
|
889
|
+
return { rel: 'preconnect', url: '/', crossOrigin: '' };
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function dedupePreloads(preloads: MetadataLink[]) {
|
|
893
|
+
const seen = new Set<string>();
|
|
894
|
+
return preloads.filter(preload => {
|
|
895
|
+
if (seen.has(preload.url)) return false;
|
|
896
|
+
seen.add(preload.url);
|
|
897
|
+
return true;
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function validateDisplay(value: FontDisplay) {
|
|
902
|
+
if (!allowedDisplayValues.has(value)) throw new Error(`Invalid font display value \`${value}\`.`);
|
|
903
|
+
return value;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function fontExt(value: string) {
|
|
907
|
+
return /\.(woff2?|eot|ttf|otf)(?:[?#]|$)/.exec(value)?.[1] ?? 'woff2';
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function fontMimeType(ext: string) {
|
|
911
|
+
if (ext === 'woff') return 'font/woff';
|
|
912
|
+
if (ext === 'ttf') return 'font/ttf';
|
|
913
|
+
if (ext === 'otf') return 'font/otf';
|
|
914
|
+
if (ext === 'eot') return 'application/vnd.ms-fontobject';
|
|
915
|
+
return 'font/woff2';
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function fontFormat(ext: string) {
|
|
919
|
+
if (ext === 'ttf') return 'truetype';
|
|
920
|
+
if (ext === 'otf') return 'opentype';
|
|
921
|
+
return ext;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function singleCssValue(values: string[]) {
|
|
925
|
+
return values.length === 1 ? values[0] : undefined;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function isRangeValue(value: string) {
|
|
929
|
+
return value.trim().includes(' ');
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function arrayValue<T>(value: T | T[]) {
|
|
933
|
+
return Array.isArray(value) ? value : [value];
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function uniqueArray<T>(items: T[]) {
|
|
937
|
+
return [...new Set(items)];
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
function customProperty(value: string) {
|
|
942
|
+
if (!/^--[-_a-zA-Z0-9]+$/.test(value)) throw new Error(`Invalid font variable name: ${value}`);
|
|
943
|
+
return value;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function cssIdentifier(value: string) {
|
|
947
|
+
if (!/^[-_a-zA-Z0-9. ]+$/.test(value)) throw new Error(`Invalid font option value: ${value}`);
|
|
948
|
+
return value;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function cssProperty(value: string) {
|
|
952
|
+
if (!/^[-_a-zA-Z0-9]+$/.test(value)) throw new Error(`Invalid CSS property: ${value}`);
|
|
953
|
+
return value;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function cssDeclarationValue(value: string) {
|
|
957
|
+
if (/[;{}]/.test(value)) throw new Error(`Invalid CSS declaration value: ${value}`);
|
|
958
|
+
return value;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function sortFontVariantValues(a: string, b: string) {
|
|
962
|
+
if (a.includes(',') && b.includes(',')) {
|
|
963
|
+
const [aPrefix, aSuffix] = a.split(',', 2);
|
|
964
|
+
const [bPrefix, bSuffix] = b.split(',', 2);
|
|
965
|
+
return aPrefix === bPrefix
|
|
966
|
+
? parseInt(aSuffix ?? '0', 10) - parseInt(bSuffix ?? '0', 10)
|
|
967
|
+
: parseInt(aPrefix ?? '0', 10) - parseInt(bPrefix ?? '0', 10);
|
|
968
|
+
}
|
|
969
|
+
return parseInt(a, 10) - parseInt(b, 10);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function escapeRegex(value: string) {
|
|
973
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
974
|
+
}
|