vinext 0.0.52 → 0.0.54
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 +1 -1
- package/dist/build/clean-output.d.ts +14 -0
- package/dist/build/clean-output.js +36 -0
- package/dist/build/clean-output.js.map +1 -0
- package/dist/build/inline-css.d.ts +7 -0
- package/dist/build/inline-css.js +50 -0
- package/dist/build/inline-css.js.map +1 -0
- package/dist/build/prerender.d.ts +6 -2
- package/dist/build/prerender.js +51 -12
- package/dist/build/prerender.js.map +1 -1
- package/dist/build/run-prerender.js +10 -1
- package/dist/build/run-prerender.js.map +1 -1
- package/dist/build/static-export.d.ts +5 -0
- package/dist/build/static-export.js +8 -3
- package/dist/build/static-export.js.map +1 -1
- package/dist/check.js +4 -0
- package/dist/check.js.map +1 -1
- package/dist/cli.js +19 -4
- package/dist/cli.js.map +1 -1
- package/dist/client/instrumentation-client-inject.d.ts +34 -0
- package/dist/client/instrumentation-client-inject.js +57 -0
- package/dist/client/instrumentation-client-inject.js.map +1 -0
- package/dist/client/navigation-runtime.d.ts +16 -2
- package/dist/client/navigation-runtime.js +16 -1
- package/dist/client/navigation-runtime.js.map +1 -1
- package/dist/client/vinext-next-data.d.ts +2 -1
- package/dist/client/vinext-next-data.js.map +1 -1
- package/dist/client/window-next.d.ts +17 -2
- package/dist/client/window-next.js.map +1 -1
- package/dist/cloudflare/tpr.js +1 -1
- package/dist/cloudflare/tpr.js.map +1 -1
- package/dist/config/config-matchers.js +2 -1
- package/dist/config/config-matchers.js.map +1 -1
- package/dist/config/next-config.d.ts +95 -4
- package/dist/config/next-config.js +173 -14
- package/dist/config/next-config.js.map +1 -1
- package/dist/deploy.js +42 -7
- package/dist/deploy.js.map +1 -1
- package/dist/entries/app-browser-entry.d.ts +11 -1
- package/dist/entries/app-browser-entry.js +16 -6
- package/dist/entries/app-browser-entry.js.map +1 -1
- package/dist/entries/app-rsc-entry.d.ts +12 -3
- package/dist/entries/app-rsc-entry.js +41 -8
- package/dist/entries/app-rsc-entry.js.map +1 -1
- package/dist/entries/app-rsc-manifest.d.ts +21 -1
- package/dist/entries/app-rsc-manifest.js +6 -4
- package/dist/entries/app-rsc-manifest.js.map +1 -1
- package/dist/entries/pages-client-entry.d.ts +4 -1
- package/dist/entries/pages-client-entry.js +40 -3
- package/dist/entries/pages-client-entry.js.map +1 -1
- package/dist/entries/pages-server-entry.js +292 -34
- package/dist/entries/pages-server-entry.js.map +1 -1
- package/dist/entries/runtime-entry-module.d.ts +1 -10
- package/dist/entries/runtime-entry-module.js +2 -12
- package/dist/entries/runtime-entry-module.js.map +1 -1
- package/dist/index.js +91 -10
- package/dist/index.js.map +1 -1
- package/dist/plugins/fonts.js +25 -2
- package/dist/plugins/fonts.js.map +1 -1
- package/dist/plugins/remove-console.d.ts +16 -0
- package/dist/plugins/remove-console.js +176 -0
- package/dist/plugins/remove-console.js.map +1 -0
- package/dist/routing/app-route-graph.d.ts +24 -1
- package/dist/routing/app-route-graph.js +52 -4
- package/dist/routing/app-route-graph.js.map +1 -1
- package/dist/routing/app-router.d.ts +2 -2
- package/dist/routing/app-router.js +2 -2
- package/dist/routing/app-router.js.map +1 -1
- package/dist/routing/file-matcher.d.ts +21 -1
- package/dist/routing/file-matcher.js +39 -1
- package/dist/routing/file-matcher.js.map +1 -1
- package/dist/routing/pages-router.d.ts +1 -1
- package/dist/routing/pages-router.js +10 -3
- package/dist/routing/pages-router.js.map +1 -1
- package/dist/routing/route-trie.js +13 -18
- package/dist/routing/route-trie.js.map +1 -1
- package/dist/routing/utils.d.ts +11 -1
- package/dist/routing/utils.js +15 -1
- package/dist/routing/utils.js.map +1 -1
- package/dist/server/api-handler.js +19 -10
- package/dist/server/api-handler.js.map +1 -1
- package/dist/server/app-browser-action-result.d.ts +16 -1
- package/dist/server/app-browser-action-result.js +15 -1
- package/dist/server/app-browser-action-result.js.map +1 -1
- package/dist/server/app-browser-entry.js +47 -28
- package/dist/server/app-browser-entry.js.map +1 -1
- package/dist/server/app-browser-navigation-controller.d.ts +2 -0
- package/dist/server/app-browser-navigation-controller.js +4 -0
- package/dist/server/app-browser-navigation-controller.js.map +1 -1
- package/dist/server/app-elements-wire.d.ts +13 -4
- package/dist/server/app-elements-wire.js +10 -1
- package/dist/server/app-elements-wire.js.map +1 -1
- package/dist/server/app-elements.d.ts +2 -2
- package/dist/server/app-elements.js +2 -2
- package/dist/server/app-elements.js.map +1 -1
- package/dist/server/app-fallback-renderer.d.ts +27 -8
- package/dist/server/app-fallback-renderer.js +19 -8
- package/dist/server/app-fallback-renderer.js.map +1 -1
- package/dist/server/app-history-state.js +6 -2
- package/dist/server/app-history-state.js.map +1 -1
- package/dist/server/app-inline-css-client.d.ts +7 -0
- package/dist/server/app-inline-css-client.js +37 -0
- package/dist/server/app-inline-css-client.js.map +1 -0
- package/dist/server/app-interception-context-header.d.ts +33 -0
- package/dist/server/app-interception-context-header.js +44 -0
- package/dist/server/app-interception-context-header.js.map +1 -0
- package/dist/server/app-mounted-slots-header.d.ts +19 -0
- package/dist/server/app-mounted-slots-header.js +40 -1
- package/dist/server/app-mounted-slots-header.js.map +1 -1
- package/dist/server/app-optimistic-routing.js +26 -18
- package/dist/server/app-optimistic-routing.js.map +1 -1
- package/dist/server/app-page-boundary-render.d.ts +1 -0
- package/dist/server/app-page-boundary-render.js +2 -0
- package/dist/server/app-page-boundary-render.js.map +1 -1
- package/dist/server/app-page-boundary.d.ts +22 -1
- package/dist/server/app-page-boundary.js +30 -3
- package/dist/server/app-page-boundary.js.map +1 -1
- package/dist/server/app-page-cache.d.ts +9 -3
- package/dist/server/app-page-cache.js +14 -8
- package/dist/server/app-page-cache.js.map +1 -1
- package/dist/server/app-page-dispatch.d.ts +13 -1
- package/dist/server/app-page-dispatch.js +136 -82
- package/dist/server/app-page-dispatch.js.map +1 -1
- package/dist/server/app-page-element-builder.d.ts +2 -1
- package/dist/server/app-page-element-builder.js +17 -30
- package/dist/server/app-page-element-builder.js.map +1 -1
- package/dist/server/app-page-execution.d.ts +1 -0
- package/dist/server/app-page-execution.js +2 -0
- package/dist/server/app-page-execution.js.map +1 -1
- package/dist/server/app-page-head.d.ts +1 -0
- package/dist/server/app-page-head.js +8 -0
- package/dist/server/app-page-head.js.map +1 -1
- package/dist/server/app-page-render-identity.d.ts +22 -0
- package/dist/server/app-page-render-identity.js +42 -0
- package/dist/server/app-page-render-identity.js.map +1 -0
- package/dist/server/app-page-render-observation.js +1 -1
- package/dist/server/app-page-render.d.ts +9 -1
- package/dist/server/app-page-render.js +8 -2
- package/dist/server/app-page-render.js.map +1 -1
- package/dist/server/app-page-request.d.ts +6 -3
- package/dist/server/app-page-request.js +5 -2
- package/dist/server/app-page-request.js.map +1 -1
- package/dist/server/app-page-response.d.ts +11 -1
- package/dist/server/app-page-response.js +16 -4
- package/dist/server/app-page-response.js.map +1 -1
- package/dist/server/app-page-route-wiring.d.ts +16 -0
- package/dist/server/app-page-route-wiring.js +25 -10
- package/dist/server/app-page-route-wiring.js.map +1 -1
- package/dist/server/app-page-stream.d.ts +12 -0
- package/dist/server/app-page-stream.js +3 -0
- package/dist/server/app-page-stream.js.map +1 -1
- package/dist/server/app-route-handler-dispatch.d.ts +1 -0
- package/dist/server/app-route-handler-dispatch.js +3 -0
- package/dist/server/app-route-handler-dispatch.js.map +1 -1
- package/dist/server/app-route-handler-execution.d.ts +1 -0
- package/dist/server/app-route-handler-execution.js +1 -0
- package/dist/server/app-route-handler-execution.js.map +1 -1
- package/dist/server/app-route-handler-response.js +38 -6
- package/dist/server/app-route-handler-response.js.map +1 -1
- package/dist/server/app-rsc-handler.d.ts +16 -3
- package/dist/server/app-rsc-handler.js +60 -11
- package/dist/server/app-rsc-handler.js.map +1 -1
- package/dist/server/app-rsc-request-normalization.d.ts +2 -1
- package/dist/server/app-rsc-request-normalization.js +6 -4
- package/dist/server/app-rsc-request-normalization.js.map +1 -1
- package/dist/server/app-segment-config.d.ts +4 -1
- package/dist/server/app-segment-config.js +6 -1
- package/dist/server/app-segment-config.js.map +1 -1
- package/dist/server/app-server-action-execution.d.ts +22 -3
- package/dist/server/app-server-action-execution.js +46 -7
- package/dist/server/app-server-action-execution.js.map +1 -1
- package/dist/server/app-ssr-entry.d.ts +6 -0
- package/dist/server/app-ssr-entry.js +57 -6
- package/dist/server/app-ssr-entry.js.map +1 -1
- package/dist/server/app-ssr-error-meta.js +3 -3
- package/dist/server/app-ssr-error-meta.js.map +1 -1
- package/dist/server/app-ssr-stream.d.ts +25 -1
- package/dist/server/app-ssr-stream.js +237 -19
- package/dist/server/app-ssr-stream.js.map +1 -1
- package/dist/server/app-static-generation.d.ts +1 -0
- package/dist/server/app-static-generation.js +2 -1
- package/dist/server/app-static-generation.js.map +1 -1
- package/dist/server/client-trace-metadata.d.ts +31 -0
- package/dist/server/client-trace-metadata.js +83 -0
- package/dist/server/client-trace-metadata.js.map +1 -0
- package/dist/server/cookie-utils.d.ts +13 -0
- package/dist/server/cookie-utils.js +20 -0
- package/dist/server/cookie-utils.js.map +1 -0
- package/dist/server/default-not-found-module.d.ts +20 -0
- package/dist/server/default-not-found-module.js +20 -0
- package/dist/server/default-not-found-module.js.map +1 -0
- package/dist/server/dev-server.d.ts +8 -1
- package/dist/server/dev-server.js +56 -11
- package/dist/server/dev-server.js.map +1 -1
- package/dist/server/headers.d.ts +5 -1
- package/dist/server/headers.js +5 -1
- package/dist/server/headers.js.map +1 -1
- package/dist/server/html.d.ts +2 -1
- package/dist/server/html.js +6 -1
- package/dist/server/html.js.map +1 -1
- package/dist/server/image-optimization.d.ts +13 -4
- package/dist/server/image-optimization.js +15 -4
- package/dist/server/image-optimization.js.map +1 -1
- package/dist/server/isr-cache.d.ts +7 -5
- package/dist/server/isr-cache.js +17 -6
- package/dist/server/isr-cache.js.map +1 -1
- package/dist/server/middleware-runtime.js +1 -2
- package/dist/server/middleware-runtime.js.map +1 -1
- package/dist/server/middleware.js +1 -1
- package/dist/server/middleware.js.map +1 -1
- package/dist/server/pages-api-route.d.ts +18 -0
- package/dist/server/pages-api-route.js +3 -1
- package/dist/server/pages-api-route.js.map +1 -1
- package/dist/server/pages-body-parser-config.d.ts +60 -0
- package/dist/server/pages-body-parser-config.js +79 -0
- package/dist/server/pages-body-parser-config.js.map +1 -0
- package/dist/server/pages-data-route.js +1 -0
- package/dist/server/pages-data-route.js.map +1 -1
- package/dist/server/pages-default-404.d.ts +31 -0
- package/dist/server/pages-default-404.js +40 -0
- package/dist/server/pages-default-404.js.map +1 -0
- package/dist/server/pages-document-initial-props.d.ts +7 -0
- package/dist/server/pages-document-initial-props.js +14 -0
- package/dist/server/pages-document-initial-props.js.map +1 -0
- package/dist/server/pages-node-compat.d.ts +10 -0
- package/dist/server/pages-node-compat.js +12 -1
- package/dist/server/pages-node-compat.js.map +1 -1
- package/dist/server/pages-page-data.d.ts +40 -0
- package/dist/server/pages-page-data.js +19 -14
- package/dist/server/pages-page-data.js.map +1 -1
- package/dist/server/pages-page-method.d.ts +48 -0
- package/dist/server/pages-page-method.js +19 -0
- package/dist/server/pages-page-method.js.map +1 -0
- package/dist/server/pages-page-response.d.ts +8 -0
- package/dist/server/pages-page-response.js +21 -11
- package/dist/server/pages-page-response.js.map +1 -1
- package/dist/server/pages-serializable-props.d.ts +25 -0
- package/dist/server/pages-serializable-props.js +69 -0
- package/dist/server/pages-serializable-props.js.map +1 -0
- package/dist/server/prerender-route-params.d.ts +14 -0
- package/dist/server/prerender-route-params.js +94 -0
- package/dist/server/prerender-route-params.js.map +1 -0
- package/dist/server/prod-server.d.ts +3 -23
- package/dist/server/prod-server.js +43 -57
- package/dist/server/prod-server.js.map +1 -1
- package/dist/server/proxy-trust.d.ts +41 -0
- package/dist/server/proxy-trust.js +70 -0
- package/dist/server/proxy-trust.js.map +1 -0
- package/dist/server/request-pipeline.d.ts +3 -3
- package/dist/server/request-pipeline.js +5 -4
- package/dist/server/request-pipeline.js.map +1 -1
- package/dist/server/seed-cache.js +12 -6
- package/dist/server/seed-cache.js.map +1 -1
- package/dist/server/server-action-not-found.js +3 -2
- package/dist/server/server-action-not-found.js.map +1 -1
- package/dist/server/static-file-cache.js +2 -1
- package/dist/server/static-file-cache.js.map +1 -1
- package/dist/server/streaming-metadata.d.ts +5 -0
- package/dist/server/streaming-metadata.js +10 -0
- package/dist/server/streaming-metadata.js.map +1 -0
- package/dist/shims/app-router-scroll-state.d.ts +14 -0
- package/dist/shims/app-router-scroll-state.js +51 -0
- package/dist/shims/app-router-scroll-state.js.map +1 -0
- package/dist/shims/app-router-scroll.d.ts +28 -0
- package/dist/shims/app-router-scroll.js +115 -0
- package/dist/shims/app-router-scroll.js.map +1 -0
- package/dist/shims/before-interactive-context.d.ts +30 -0
- package/dist/shims/before-interactive-context.js +10 -0
- package/dist/shims/before-interactive-context.js.map +1 -0
- package/dist/shims/cache-runtime.d.ts +1 -1
- package/dist/shims/cache-runtime.js +14 -1
- package/dist/shims/cache-runtime.js.map +1 -1
- package/dist/shims/cache.d.ts +6 -0
- package/dist/shims/cache.js +7 -0
- package/dist/shims/cache.js.map +1 -1
- package/dist/shims/default-not-found.d.ts +12 -0
- package/dist/shims/default-not-found.js +61 -0
- package/dist/shims/default-not-found.js.map +1 -0
- package/dist/shims/error.js +3 -0
- package/dist/shims/error.js.map +1 -1
- package/dist/shims/font-local.d.ts +5 -0
- package/dist/shims/font-local.js +6 -2
- package/dist/shims/font-local.js.map +1 -1
- package/dist/shims/head.js +4 -4
- package/dist/shims/head.js.map +1 -1
- package/dist/shims/headers.d.ts +13 -2
- package/dist/shims/headers.js +73 -22
- package/dist/shims/headers.js.map +1 -1
- package/dist/shims/image.d.ts +1 -1
- package/dist/shims/image.js +4 -4
- package/dist/shims/image.js.map +1 -1
- package/dist/shims/internal/app-route-detection.d.ts +37 -0
- package/dist/shims/internal/app-route-detection.js +69 -0
- package/dist/shims/internal/app-route-detection.js.map +1 -0
- package/dist/shims/internal/pages-data-target.d.ts +58 -0
- package/dist/shims/internal/pages-data-target.js +91 -0
- package/dist/shims/internal/pages-data-target.js.map +1 -0
- package/dist/shims/internal/pages-data-url.d.ts +42 -0
- package/dist/shims/internal/pages-data-url.js +73 -0
- package/dist/shims/internal/pages-data-url.js.map +1 -0
- package/dist/shims/link.d.ts +18 -2
- package/dist/shims/link.js +129 -15
- package/dist/shims/link.js.map +1 -1
- package/dist/shims/metadata.d.ts +9 -7
- package/dist/shims/metadata.js +70 -7
- package/dist/shims/metadata.js.map +1 -1
- package/dist/shims/navigation.d.ts +1 -2
- package/dist/shims/navigation.js +94 -20
- package/dist/shims/navigation.js.map +1 -1
- package/dist/shims/router.d.ts +5 -0
- package/dist/shims/router.js +389 -80
- package/dist/shims/router.js.map +1 -1
- package/dist/shims/script.d.ts +11 -1
- package/dist/shims/script.js +158 -15
- package/dist/shims/script.js.map +1 -1
- package/dist/shims/server.js +1 -0
- package/dist/shims/server.js.map +1 -1
- package/dist/shims/url-utils.d.ts +2 -1
- package/dist/shims/url-utils.js +15 -4
- package/dist/shims/url-utils.js.map +1 -1
- package/dist/utils/html-limited-bots.d.ts +5 -0
- package/dist/utils/html-limited-bots.js +15 -0
- package/dist/utils/html-limited-bots.js.map +1 -0
- package/dist/utils/path.d.ts +13 -0
- package/dist/utils/path.js +16 -0
- package/dist/utils/path.js.map +1 -0
- package/dist/utils/query.d.ts +6 -0
- package/dist/utils/query.js +10 -1
- package/dist/utils/query.js.map +1 -1
- package/package.json +1 -1
package/dist/plugins/fonts.js
CHANGED
|
@@ -264,6 +264,9 @@ function parseGoogleFontImportClause(clause) {
|
|
|
264
264
|
function propertyNameToGoogleFontFamily(prop) {
|
|
265
265
|
return prop.replace(/_/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2");
|
|
266
266
|
}
|
|
267
|
+
function escapeRegExp(value) {
|
|
268
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
269
|
+
}
|
|
267
270
|
/**
|
|
268
271
|
* Fetch Google Fonts CSS, download .woff2 files, cache locally, and return
|
|
269
272
|
* @font-face CSS with local file references.
|
|
@@ -647,7 +650,9 @@ function createLocalFontsPlugin() {
|
|
|
647
650
|
if (!id.match(/\.(tsx?|jsx?|mjs)$/)) return null;
|
|
648
651
|
if (!code.includes("next/font/local")) return null;
|
|
649
652
|
if (id.includes("font-local")) return null;
|
|
650
|
-
|
|
653
|
+
const importMatch = /\bimport\s+([A-Za-z_$][A-Za-z0-9_$]*)\s+from\s*(['"])next\/font\/local\2/.exec(code);
|
|
654
|
+
if (!importMatch) return null;
|
|
655
|
+
const localFontIdentifier = importMatch[1];
|
|
651
656
|
const s = new MagicString(code);
|
|
652
657
|
let hasChanges = false;
|
|
653
658
|
let fontImportCounter = 0;
|
|
@@ -663,8 +668,26 @@ function createLocalFontsPlugin() {
|
|
|
663
668
|
s.overwrite(matchStart, matchEnd, `${prefix}${varName}`);
|
|
664
669
|
hasChanges = true;
|
|
665
670
|
}
|
|
671
|
+
const localFontCallRe = new RegExp(String.raw`(?:^|[;{}\n])\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*(?::[^=]+)?=\s*${escapeRegExp(localFontIdentifier)}\s*\(\s*(?=\{)`, "g");
|
|
672
|
+
const familyPayloadInsertions = /* @__PURE__ */ new Set();
|
|
673
|
+
let localFontCallMatch;
|
|
674
|
+
while ((localFontCallMatch = localFontCallRe.exec(code)) !== null) {
|
|
675
|
+
const bindingName = localFontCallMatch[1];
|
|
676
|
+
const objRange = _findBalancedObject(code, localFontCallRe.lastIndex);
|
|
677
|
+
if (!objRange) continue;
|
|
678
|
+
if (_findCallEnd(code, objRange[1]) === null) continue;
|
|
679
|
+
const insertAt = objRange[1] - 1;
|
|
680
|
+
if (familyPayloadInsertions.has(insertAt)) continue;
|
|
681
|
+
const optionsStr = code.slice(objRange[0], objRange[1]);
|
|
682
|
+
if (/(?:^|[,{])\s*_vinext\s*:/.test(optionsStr)) continue;
|
|
683
|
+
const beforeClosingBrace = optionsStr.slice(0, -1).trim();
|
|
684
|
+
const separator = beforeClosingBrace.endsWith("{") || beforeClosingBrace.endsWith(",") ? "" : ", ";
|
|
685
|
+
s.appendLeft(insertAt, `${separator}_vinext: { font: { family: ${JSON.stringify(bindingName)} } }`);
|
|
686
|
+
familyPayloadInsertions.add(insertAt);
|
|
687
|
+
hasChanges = true;
|
|
688
|
+
}
|
|
666
689
|
if (!hasChanges) return null;
|
|
667
|
-
s.prepend(imports.join("\n") + "\n");
|
|
690
|
+
if (imports.length > 0) s.prepend(imports.join("\n") + "\n");
|
|
668
691
|
return {
|
|
669
692
|
code: s.toString(),
|
|
670
693
|
map: s.generateMap({ hires: "boundary" })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fonts.js","names":[],"sources":["../../src/plugins/fonts.ts"],"sourcesContent":["/**\n * vinext font plugins\n *\n * Exports two Vite plugins:\n *\n * `createGoogleFontsPlugin` — vinext:google-fonts\n * 1. Rewrites named `next/font/google` imports/exports to tiny virtual modules\n * that export only the requested fonts plus any utility exports. This lets us\n * delete the generated ~1,900-line runtime catalog while keeping ESM import\n * semantics intact.\n * 2. During production builds, fetches Google Fonts CSS + font files, caches\n * them locally under `.vinext/fonts/`, and injects `_vinext.font` into\n * statically analyzable font loader calls so fonts are served from the\n * deployed origin rather than fonts.googleapis.com. Static calls also\n * receive adjusted fallback CSS when Next.js-compatible fallback metrics\n * exist for the selected Google Font.\n *\n * `createLocalFontsPlugin` — vinext:local-fonts\n * When a source file calls localFont({ src: \"./font.woff2\" }) or\n * localFont({ src: [{ path: \"./font.woff2\" }] }), the relative paths\n * won't resolve in the browser because the CSS is injected at runtime.\n * This plugin rewrites those path strings into Vite asset import references\n * so that both dev (/@fs/...) and prod (/assets/font-xxx.woff2) URLs are\n * correct.\n */\n\nimport type { Plugin } from \"vite\";\nimport { parseAst } from \"vite\";\nimport path from \"node:path\";\nimport fs from \"node:fs\";\nimport MagicString from \"magic-string\";\nimport {\n buildFallbackFontFace,\n getFallbackFontOverrideMetrics,\n} from \"../build/google-fonts/fallback-metrics.js\";\nimport { validateGoogleFontOptions } from \"../build/google-fonts/validate.js\";\nimport { getFontAxes } from \"../build/google-fonts/get-axes.js\";\nimport { buildGoogleFontsUrl } from \"../build/google-fonts/build-url.js\";\nimport { CONTENT_TYPES } from \"../server/static-file-cache.js\";\nimport { ASSET_PREFIX_URL_DIR } from \"../utils/asset-prefix.js\";\n\n/**\n * Thrown when Google Fonts returns a non-2xx response. Distinct from a raw\n * `fetch` rejection (network error, DNS failure, AbortError) so the call\n * site can decide whether to surface as a build error or fall through to\n * the runtime CDN path.\n */\nclass GoogleFontsHttpError extends Error {\n constructor(\n public readonly url: string,\n public readonly status: number,\n public readonly responseBody: string,\n ) {\n super(`Google Fonts returned HTTP ${status} for ${url}`);\n this.name = \"GoogleFontsHttpError\";\n }\n}\n\n// ── Virtual module IDs ────────────────────────────────────────────────────────\n\nexport const VIRTUAL_GOOGLE_FONTS = \"virtual:vinext-google-fonts\";\nexport const RESOLVED_VIRTUAL_GOOGLE_FONTS = \"\\0\" + VIRTUAL_GOOGLE_FONTS;\n\n// ── Constants ─────────────────────────────────────────────────────────────────\n\n// IMPORTANT: keep this set in sync with the non-default exports from\n// packages/vinext/src/shims/font-google.ts (and its re-export barrel).\nconst GOOGLE_FONT_UTILITY_EXPORTS = new Set([\n \"buildGoogleFontsUrl\",\n \"getSSRFontLinks\",\n \"getSSRFontStyles\",\n \"getSSRFontPreloads\",\n \"createFontLoader\",\n]);\n\n/**\n * Served URL prefix for self-hosted Google Font files.\n *\n * `fetchAndCacheFont()` downloads .woff2 files into `<root>/.vinext/fonts/`\n * and writes an `@font-face` CSS snippet whose `src: url(...)` references\n * the files by absolute filesystem path — convenient for disk, unusable at\n * runtime because browsers resolve relative to the origin. Before the CSS\n * is embedded in the bundle as `_vinext.font.selfHostedCSS`, the filesystem\n * prefix is rewritten to this URL prefix by `_rewriteCachedFontCssToServedUrls()`,\n * and the matching `writeBundle` hook in `createGoogleFontsPlugin` copies\n * the font files into `<clientOutDir>/<assetsDir>/_vinext_fonts/` so the\n * rewritten URL actually resolves against the origin at request time.\n *\n * The leading `_` keeps the namespace distinct from Vite's content-hashed\n * asset names (which are emitted flat into `<assetsDir>/`) and from any\n * user-provided public files.\n */\nconst VINEXT_FONT_URL_NAMESPACE = \"_vinext_fonts\";\nconst MAX_GOOGLE_FONTS_ERROR_BODY_LENGTH = 500;\n\nfunction formatGoogleFontsErrorBody(body: string): string {\n const trimmed = body.trim();\n if (!trimmed) return \"(empty response body)\";\n if (trimmed.length <= MAX_GOOGLE_FONTS_ERROR_BODY_LENGTH) return trimmed;\n const omitted = trimmed.length - MAX_GOOGLE_FONTS_ERROR_BODY_LENGTH;\n return `${trimmed.slice(0, MAX_GOOGLE_FONTS_ERROR_BODY_LENGTH)}\\n... (truncated ${omitted} characters)`;\n}\n\n/**\n * Rewrite absolute filesystem paths in cached Google Fonts CSS so the\n * `@font-face { src: url(...) }` references point at the served URL the\n * plugin's `writeBundle` hook copies the font files to.\n *\n * This is called once per transform, before the CSS string is embedded in\n * the bundle as `_vinext.font.selfHostedCSS`. Every downstream consumer reads\n * from the same rewritten CSS: the injected `<style data-vinext-fonts>` block, the\n * HTML body's `<link rel=\"preload\">` tags (via `collectFontPreloadsFromCSS`\n * in `shims/font-google-base.ts`), and the HTTP `Link:` response header\n * (via `buildAppPageFontLinkHeader` in `server/app-page-execution.ts`).\n *\n * Without this rewrite, all three emit the dev-machine filesystem path\n * (e.g. `/home/user/project/.vinext/fonts/geist-<hash>/geist-<hash>.woff2`)\n * and any production request fetches `<origin>/home/user/...` → 404.\n *\n * `assetsDir` must match whatever Vite has resolved for\n * `build.assetsDir` on the client environment — otherwise the embedded\n * CSS URLs and the files emitted by the `writeBundle` hook would diverge\n * and a user who customizes `build.assetsDir` (e.g. to `\"static\"`) would\n * see 404s on every preload. The call site in `injectSelfHostedCss`\n * passes the resolved value through from plugin state. The default is\n * kept only so the exported helper can be driven directly from unit\n * tests without synthesizing a full plugin context.\n *\n * Uses split/join rather than regex because `cacheDir` is an absolute\n * filesystem path that may contain regex metacharacters on unusual\n * filesystems.\n */\nexport function _rewriteCachedFontCssToServedUrls(\n css: string,\n cacheDir: string,\n assetsDir: string = DEFAULT_ASSETS_DIR,\n): string {\n if (!cacheDir || !css.includes(cacheDir)) return css;\n const prefix = assetsDir || DEFAULT_ASSETS_DIR;\n return css.split(cacheDir).join(`/${prefix}/${VINEXT_FONT_URL_NAMESPACE}`);\n}\n\n/**\n * Default `build.assetsDir` — matches vinext's resolved default in\n * `resolveAssetsDir(\"\")` (Next.js's canonical convention). Used as the\n * fallback for the `assetsDir` parameter of\n * `_rewriteCachedFontCssToServedUrls` so the exported helper can be unit\n * tested without synthesizing plugin state. Production call sites thread\n * the real `envConfig.build.assetsDir` resolved by Vite through so that\n * the embedded CSS URLs always match the directory the `writeBundle`\n * hook copies the font files into.\n */\nconst DEFAULT_ASSETS_DIR = ASSET_PREFIX_URL_DIR;\n\n// ── Types ─────────────────────────────────────────────────────────────────────\n\ntype GoogleFontNamedSpecifier = {\n imported: string;\n local: string;\n isType: boolean;\n raw: string;\n};\n\n// ── Helpers shared with index.ts ──────────────────────────────────────────────\n\n/**\n * Safely parse a static JS object literal string into a plain object.\n * Uses Vite's parseAst (Rollup/acorn) so no code is ever evaluated.\n * Returns null if the expression contains anything dynamic (function calls,\n * template literals, identifiers, computed properties, etc.).\n *\n * Supports: string literals, numeric literals, boolean literals,\n * arrays of the above, and nested object literals.\n */\nexport function parseStaticObjectLiteral(objectStr: string): Record<string, unknown> | null {\n let ast: ReturnType<typeof parseAst>;\n try {\n // Wrap in parens so the parser treats `{…}` as an expression, not a block\n ast = parseAst(`(${objectStr})`);\n } catch {\n return null;\n }\n\n // The AST should be: Program > ExpressionStatement > ObjectExpression\n const body = ast.body;\n if (body.length !== 1 || body[0].type !== \"ExpressionStatement\") return null;\n\n const expr = body[0].expression;\n if (expr.type !== \"ObjectExpression\") return null;\n\n const result = extractStaticValue(expr);\n return result === undefined ? null : (result as Record<string, unknown>);\n}\n\n/**\n * Recursively extract a static value from an ESTree AST node.\n * Returns undefined (not null) if the node contains any dynamic expression.\n *\n * Uses `any` for the node parameter because Rollup's internal ESTree types\n * (estree.Expression, estree.ObjectExpression, etc.) aren't re-exported by Vite,\n * and the recursive traversal touches many different node shapes.\n */\n// oxlint-disable-next-line @typescript-eslint/no-explicit-any\nfunction extractStaticValue(node: any): unknown {\n switch (node.type) {\n case \"Literal\":\n // String, number, boolean, null\n return node.value;\n\n case \"UnaryExpression\":\n // Handle negative numbers: -1, -3.14\n if (\n node.operator === \"-\" &&\n node.argument?.type === \"Literal\" &&\n typeof node.argument.value === \"number\"\n ) {\n return -node.argument.value;\n }\n return undefined;\n\n case \"ArrayExpression\": {\n const arr: unknown[] = [];\n for (const elem of node.elements) {\n if (!elem) return undefined; // sparse array\n const val = extractStaticValue(elem);\n if (val === undefined) return undefined;\n arr.push(val);\n }\n return arr;\n }\n\n case \"ObjectExpression\": {\n const obj: Record<string, unknown> = {};\n for (const prop of node.properties) {\n if (prop.type !== \"Property\") return undefined; // SpreadElement etc.\n if (prop.computed) return undefined; // [expr]: val\n\n // Key can be Identifier (unquoted) or Literal (quoted)\n let key: string;\n if (prop.key.type === \"Identifier\") {\n key = prop.key.name;\n } else if (prop.key.type === \"Literal\" && typeof prop.key.value === \"string\") {\n key = prop.key.value;\n } else {\n return undefined;\n }\n\n const val = extractStaticValue(prop.value);\n if (val === undefined) return undefined;\n obj[key] = val;\n }\n return obj;\n }\n\n default:\n // TemplateLiteral, CallExpression, Identifier, etc. — reject\n return undefined;\n }\n}\n\n// ── Virtual module encoding/decoding ─────────────────────────────────────────\n\nfunction encodeGoogleFontsVirtualId(payload: {\n hasDefault: boolean;\n fonts: string[];\n utilities: string[];\n}): string {\n const params = new URLSearchParams();\n if (payload.hasDefault) params.set(\"default\", \"1\");\n if (payload.fonts.length > 0) params.set(\"fonts\", payload.fonts.join(\",\"));\n if (payload.utilities.length > 0) params.set(\"utilities\", payload.utilities.join(\",\"));\n return `${VIRTUAL_GOOGLE_FONTS}?${params.toString()}`;\n}\n\nfunction parseGoogleFontsVirtualId(id: string): {\n hasDefault: boolean;\n fonts: string[];\n utilities: string[];\n} | null {\n const cleanId = id.startsWith(\"\\0\") ? id.slice(1) : id;\n if (!cleanId.startsWith(VIRTUAL_GOOGLE_FONTS)) return null;\n const queryIndex = cleanId.indexOf(\"?\");\n const params = new URLSearchParams(queryIndex === -1 ? \"\" : cleanId.slice(queryIndex + 1));\n return {\n hasDefault: params.get(\"default\") === \"1\",\n fonts:\n params\n .get(\"fonts\")\n ?.split(\",\")\n .map((value) => value.trim())\n .filter(Boolean) ?? [],\n utilities:\n params\n .get(\"utilities\")\n ?.split(\",\")\n .map((value) => value.trim())\n .filter(Boolean) ?? [],\n };\n}\n\nexport function generateGoogleFontsVirtualModule(\n id: string,\n fontGoogleShimPath: string,\n): string | null {\n const payload = parseGoogleFontsVirtualId(id);\n if (!payload) return null;\n\n const utilities = Array.from(new Set(payload.utilities));\n const fonts = Array.from(new Set(payload.fonts));\n const lines: string[] = [];\n\n lines.push(`import { createFontLoader } from ${JSON.stringify(fontGoogleShimPath)};`);\n\n const reExports: string[] = [];\n if (payload.hasDefault) reExports.push(\"default\");\n reExports.push(...utilities);\n if (reExports.length > 0) {\n lines.push(`export { ${reExports.join(\", \")} } from ${JSON.stringify(fontGoogleShimPath)};`);\n }\n\n for (const fontName of fonts) {\n const family = fontName.replace(/_/g, \" \");\n lines.push(\n `export const ${fontName} = /*#__PURE__*/ createFontLoader(${JSON.stringify(family)});`,\n );\n }\n\n lines.push(\"\");\n return lines.join(\"\\n\");\n}\n\n// ── Import clause parsers ─────────────────────────────────────────────────────\n\nfunction parseGoogleFontNamedSpecifiers(\n specifiersStr: string,\n forceType = false,\n): GoogleFontNamedSpecifier[] {\n return specifiersStr\n .split(\",\")\n .map((spec) => spec.trim())\n .filter(Boolean)\n .map((raw) => {\n const isType = forceType || raw.startsWith(\"type \");\n const valueSpec = isType ? raw.replace(/^type\\s+/, \"\") : raw;\n const asParts = valueSpec.split(/\\s+as\\s+/);\n const imported = asParts[0]?.trim() ?? \"\";\n const local = (asParts[1] || asParts[0] || \"\").trim();\n return { imported, local, isType, raw };\n })\n .filter((spec) => spec.imported.length > 0 && spec.local.length > 0);\n}\n\nfunction parseGoogleFontImportClause(clause: string): {\n defaultLocal: string | null;\n namespaceLocal: string | null;\n named: GoogleFontNamedSpecifier[];\n} {\n const trimmed = clause.trim();\n\n if (trimmed.startsWith(\"type \")) {\n const braceStart = trimmed.indexOf(\"{\");\n const braceEnd = trimmed.lastIndexOf(\"}\");\n if (braceStart === -1 || braceEnd === -1) {\n return { defaultLocal: null, namespaceLocal: null, named: [] };\n }\n return {\n defaultLocal: null,\n namespaceLocal: null,\n named: parseGoogleFontNamedSpecifiers(trimmed.slice(braceStart + 1, braceEnd), true),\n };\n }\n\n const braceStart = trimmed.indexOf(\"{\");\n const braceEnd = trimmed.lastIndexOf(\"}\");\n if (braceStart !== -1 && braceEnd !== -1) {\n const beforeNamed = trimmed.slice(0, braceStart).trim().replace(/,\\s*$/, \"\").trim();\n return {\n defaultLocal: beforeNamed || null,\n namespaceLocal: null,\n named: parseGoogleFontNamedSpecifiers(trimmed.slice(braceStart + 1, braceEnd)),\n };\n }\n\n const commaIndex = trimmed.indexOf(\",\");\n if (commaIndex !== -1) {\n const defaultLocal = trimmed.slice(0, commaIndex).trim() || null;\n const rest = trimmed.slice(commaIndex + 1).trim();\n if (rest.startsWith(\"* as \")) {\n return {\n defaultLocal,\n namespaceLocal: rest.slice(\"* as \".length).trim() || null,\n named: [],\n };\n }\n }\n\n if (trimmed.startsWith(\"* as \")) {\n return {\n defaultLocal: null,\n namespaceLocal: trimmed.slice(\"* as \".length).trim() || null,\n named: [],\n };\n }\n\n return {\n defaultLocal: trimmed || null,\n namespaceLocal: null,\n named: [],\n };\n}\n\nfunction propertyNameToGoogleFontFamily(prop: string): string {\n return prop.replace(/_/g, \" \").replace(/([a-z])([A-Z])/g, \"$1 $2\");\n}\n\n// ── Font fetching and caching ─────────────────────────────────────────────────\n\n/**\n * Fetch Google Fonts CSS, download .woff2 files, cache locally, and return\n * @font-face CSS with local file references.\n *\n * Cache dir structure: .vinext/fonts/<family-hash>/\n * - style.css (the rewritten @font-face CSS)\n * - *.woff2 (downloaded font files)\n */\nasync function fetchAndCacheFont(\n cssUrl: string,\n family: string,\n cacheDir: string,\n): Promise<string> {\n // Use a hash of the URL for the cache key\n const { createHash } = await import(\"node:crypto\");\n const urlHash = createHash(\"md5\").update(cssUrl).digest(\"hex\").slice(0, 12);\n const fontDir = path.join(cacheDir, `${family.toLowerCase().replace(/\\s+/g, \"-\")}-${urlHash}`);\n\n // Check if already cached\n const cachedCSSPath = path.join(fontDir, \"style.css\");\n if (fs.existsSync(cachedCSSPath)) {\n return fs.readFileSync(cachedCSSPath, \"utf-8\");\n }\n\n // Fetch CSS from Google Fonts (woff2 user-agent gives woff2 URLs)\n const cssResponse = await fetch(cssUrl, {\n headers: {\n \"User-Agent\":\n \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36\",\n },\n });\n if (!cssResponse.ok) {\n // Include the response body when Google rejected the request so the\n // caller can see why (the body usually contains a one-line CSS comment\n // identifying the bad axis or family).\n const body = await cssResponse.text().catch(() => \"\");\n throw new GoogleFontsHttpError(cssUrl, cssResponse.status, body);\n }\n let css = await cssResponse.text();\n\n // Extract all font file URLs\n const urlRe = /url\\((https:\\/\\/fonts\\.gstatic\\.com\\/[^)]+)\\)/g;\n const urls = new Map<string, string>(); // original URL -> local filename\n let urlMatch;\n while ((urlMatch = urlRe.exec(css)) !== null) {\n const fontUrl = urlMatch[1];\n if (!urls.has(fontUrl)) {\n const ext = fontUrl.includes(\".woff2\")\n ? \".woff2\"\n : fontUrl.includes(\".woff\")\n ? \".woff\"\n : \".ttf\";\n const fileHash = createHash(\"md5\").update(fontUrl).digest(\"hex\").slice(0, 8);\n urls.set(fontUrl, `${family.toLowerCase().replace(/\\s+/g, \"-\")}-${fileHash}${ext}`);\n }\n }\n\n // Download font files\n fs.mkdirSync(fontDir, { recursive: true });\n for (const [fontUrl, filename] of urls) {\n const filePath = path.join(fontDir, filename);\n if (!fs.existsSync(filePath)) {\n const fontResponse = await fetch(fontUrl);\n if (fontResponse.ok) {\n const buffer = Buffer.from(await fontResponse.arrayBuffer());\n fs.writeFileSync(filePath, buffer);\n }\n }\n // Rewrite every remote Google Fonts CDN URL in the cached CSS to the\n // absolute filesystem path of the locally-downloaded font file. This\n // cache file is read back by the plugin and then run through\n // `_rewriteCachedFontCssToServedUrls()` at embed time, which replaces\n // the absolute `cacheDir` prefix with the served URL namespace under\n // `/<assetsDir>/_vinext_fonts/`. The filesystem path is only the\n // on-disk intermediate form — it must never reach the bundle, the\n // injected `<style data-vinext-fonts>` block, the HTML `<link\n // rel=\"preload\">` tags, or the HTTP `Link:` response header. An\n // earlier version of this code claimed \"Vite will resolve /@fs/ for\n // dev, or asset for build\", which was never true: the CSS is\n // embedded as a JavaScript string literal and Vite's asset pipeline\n // does not scan string literals. Do not resurrect that assumption.\n css = css.split(fontUrl).join(filePath.replaceAll(\"\\\\\", \"/\"));\n }\n\n // Cache the rewritten CSS\n fs.writeFileSync(cachedCSSPath, css);\n return css;\n}\n\n// ── Plugin factories ──────────────────────────────────────────────────────────\n\n/**\n * Create the `vinext:google-fonts` Vite plugin.\n *\n * @param fontGoogleShimPath - Absolute path to the font-google shim module\n * (either `.ts` in source or `.js` in built packages). Resolved by the caller\n * so the plugin file has no dependency on `__dirname`.\n * @param shimsDir - Absolute path to the shims directory. Used to skip shim\n * files from transform (they contain `next/font/google` references that must\n * not be rewritten).\n */\n\n/**\n * Scan `code` forward from `searchStart` for a `{...}` object literal that\n * may contain arbitrarily nested braces. Returns `[objStart, objEnd]` where\n * `code[objStart] === '{'` and `code[objEnd - 1] === '}'`, or `null` if no\n * balanced object is found.\n *\n * String literals (single-quoted, double-quoted, and backtick template\n * literals including `${...}` interpolations) are fully skipped so that brace\n * characters inside string values do not affect the depth count.\n */\nexport function _findBalancedObject(code: string, searchStart: number): [number, number] | null {\n let i = searchStart;\n // Skip leading whitespace before the opening brace\n while (\n i < code.length &&\n (code[i] === \" \" || code[i] === \"\\t\" || code[i] === \"\\n\" || code[i] === \"\\r\")\n ) {\n i++;\n }\n if (i >= code.length || code[i] !== \"{\") return null;\n const objStart = i;\n let depth = 0;\n while (i < code.length) {\n const ch = code[i];\n if (ch === '\"' || ch === \"'\") {\n // Skip a single- or double-quoted string literal, respecting backslash escapes.\n const quote = ch;\n i++;\n while (i < code.length) {\n const sc = code[i];\n if (sc === \"\\\\\") {\n i += 2; // skip escaped character\n } else if (sc === quote) {\n i++;\n break;\n } else {\n i++;\n }\n }\n } else if (ch === \"`\") {\n // Skip a template literal, including ${...} interpolation blocks.\n // We need to track brace depth inside interpolations so that a `}`\n // that closes an interpolation isn't mistaken for closing the object.\n i++; // consume the opening backtick\n while (i < code.length) {\n const tc = code[i];\n if (tc === \"\\\\\") {\n i += 2; // skip escape sequence\n } else if (tc === \"`\") {\n i++; // end of template literal\n break;\n } else if (tc === \"$\" && code[i + 1] === \"{\") {\n // Enter a ${...} interpolation: scan forward tracking nested braces.\n i += 2; // consume '${'\n let exprDepth = 1;\n while (i < code.length && exprDepth > 0) {\n const ec = code[i];\n if (ec === \"{\") {\n exprDepth++;\n i++;\n } else if (ec === \"}\") {\n exprDepth--;\n i++;\n } else if (ec === '\"' || ec === \"'\") {\n // Quoted string inside interpolation — skip it\n const q = ec;\n i++;\n while (i < code.length) {\n if (code[i] === \"\\\\\") {\n i += 2;\n } else if (code[i] === q) {\n i++;\n break;\n } else {\n i++;\n }\n }\n } else if (ec === \"`\") {\n // Nested template literal inside interpolation — skip it\n // (simple depth-1 skip; deeply nested templates are rare in font options)\n i++;\n while (i < code.length) {\n if (code[i] === \"\\\\\") {\n i += 2;\n } else if (code[i] === \"`\") {\n i++;\n break;\n } else {\n i++;\n }\n }\n } else {\n i++;\n }\n }\n } else {\n i++;\n }\n }\n } else if (ch === \"{\") {\n depth++;\n i++;\n } else if (ch === \"}\") {\n depth--;\n i++;\n if (depth === 0) return [objStart, i];\n } else {\n i++;\n }\n }\n return null; // unbalanced\n}\n\n/**\n * Given the index just past the closing `}` of an options object, skip\n * optional whitespace and return the index after the closing `)`.\n * Returns `null` if the next non-whitespace character is not `)`.\n */\nexport function _findCallEnd(code: string, objEnd: number): number | null {\n let i = objEnd;\n while (\n i < code.length &&\n (code[i] === \" \" || code[i] === \"\\t\" || code[i] === \"\\n\" || code[i] === \"\\r\")\n ) {\n i++;\n }\n if (i >= code.length || code[i] !== \")\") return null;\n return i + 1;\n}\n\nexport function createGoogleFontsPlugin(fontGoogleShimPath: string, shimsDir: string): Plugin {\n // Vite does not bind `this` to the plugin object when calling hooks, so\n // plugin state must be held in closure variables rather than as properties.\n const fontCache = new Map<string, string>(); // url -> local @font-face CSS\n let cacheDir = \"\";\n\n return {\n name: \"vinext:google-fonts\",\n enforce: \"pre\",\n\n configResolved(config) {\n cacheDir = path.join(config.root, \".vinext\", \"fonts\");\n },\n\n // Dev-mode equivalent of the production `writeBundle` copy step. In dev\n // there is no client output directory to copy files into, so the cached\n // .vinext/fonts/ tree is served directly under the same URL prefix that\n // `_rewriteCachedFontCssToServedUrls()` embeds into the @font-face CSS\n // (`/<assetsDir>/_vinext_fonts/...`). Without this hook the rewritten\n // URLs 404 — and once `_vinext.font.selfHostedCSS` is injected, the shim no longer\n // emits the fonts.googleapis.com `<link>`, so a 404 here means no\n // glyphs render at all (no CDN fallback path).\n configureServer(server) {\n if (!cacheDir) return;\n const assetsDir =\n server.environments?.client?.config?.build?.assetsDir ??\n server.config?.build?.assetsDir ??\n DEFAULT_ASSETS_DIR;\n const urlPrefix = `/${assetsDir}/${VINEXT_FONT_URL_NAMESPACE}/`;\n server.middlewares.use((req, res, next) => {\n const url = req.url;\n if (!url || !url.startsWith(urlPrefix)) return next();\n const rawPath = url.slice(urlPrefix.length).split(\"?\")[0];\n let decoded: string;\n try {\n decoded = decodeURIComponent(rawPath);\n } catch {\n return next();\n }\n const filePath = path.resolve(cacheDir, decoded);\n // Path traversal guard — `decoded` came from the URL, so refuse\n // anything that escapes the cache root (e.g. `..%2F..%2Fetc/passwd`).\n if (filePath !== cacheDir && !filePath.startsWith(cacheDir + path.sep)) {\n return next();\n }\n fs.stat(filePath, (err, stat) => {\n if (err || !stat.isFile()) return next();\n const ext = path.extname(filePath).toLowerCase();\n // CONTENT_TYPES is the same map prod-server uses, so fonts get\n // identical MIME types in dev and prod. fetchAndCacheFont only\n // ever writes .woff2/.woff/.ttf, all of which are covered.\n res.setHeader(\"Content-Type\", CONTENT_TYPES[ext] ?? \"application/octet-stream\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n fs.createReadStream(filePath).pipe(res);\n });\n });\n },\n\n transform: {\n // Hook filter: only invoke JS when code contains 'next/font/google'.\n // This still eliminates nearly all Rust-to-JS calls since very few files\n // import from next/font/google.\n filter: {\n id: {\n include: /\\.(tsx?|jsx?|mjs)$/,\n },\n code: \"next/font/google\",\n },\n async handler(code, id) {\n // Defensive guard — duplicates filter logic\n if (id.startsWith(\"\\0\")) return null;\n if (!id.match(/\\.(tsx?|jsx?|mjs)$/)) return null;\n if (!code.includes(\"next/font/google\")) return null;\n if (id.startsWith(shimsDir)) return null;\n\n // Read the resolved `build.assetsDir` from the current Vite\n // environment so it can be closed over by the inner\n // `injectSelfHostedCss` helper (a plain function declaration\n // where `this` is untyped). Captured at the top of the hook so\n // a single handler invocation always threads one consistent\n // value through every font-loader call site it rewrites.\n const transformAssetsDir = this.environment?.config?.build?.assetsDir ?? DEFAULT_ASSETS_DIR;\n\n const s = new MagicString(code);\n let hasChanges = false;\n let proxyImportCounter = 0;\n const overwrittenRanges: Array<[number, number]> = [];\n const fontLocals = new Map<string, string>();\n const proxyObjectLocals = new Set<string>();\n\n // The clause is a sequence of either a brace block (`\\{[^}]*?\\}` —\n // newlines allowed inside, but `[^}]` keeps it from spanning past\n // the matching close brace) or a single non-`;` non-`\\n` char.\n // Effect: multi-line bracket imports (Prettier wraps past\n // `printWidth`) match, but a preceding semicolon-less line\n // (e.g. `import type { Metadata } from 'next'`) can't be swallowed\n // into the clause via newline crossings. Both shapes used to fail\n // silently — the rewrite was skipped because the resulting clause\n // wasn't a valid single import.\n const importRe =\n /^[ \\t]*import\\s+((?:\\{[^}]*?\\}|[^;\\n])+?)\\s+from\\s*([\"'])next\\/font\\/google\\2\\s*;?/gm;\n let importMatch;\n while ((importMatch = importRe.exec(code)) !== null) {\n const [fullMatch, clause] = importMatch;\n const matchStart = importMatch.index;\n const matchEnd = matchStart + fullMatch.length;\n const parsed = parseGoogleFontImportClause(clause);\n const utilityImports = parsed.named.filter(\n (spec) => !spec.isType && GOOGLE_FONT_UTILITY_EXPORTS.has(spec.imported),\n );\n const fontImports = parsed.named.filter(\n (spec) => !spec.isType && !GOOGLE_FONT_UTILITY_EXPORTS.has(spec.imported),\n );\n\n if (parsed.defaultLocal) {\n proxyObjectLocals.add(parsed.defaultLocal);\n }\n for (const fontImport of fontImports) {\n fontLocals.set(fontImport.local, fontImport.imported);\n }\n\n if (fontImports.length > 0) {\n const virtualId = encodeGoogleFontsVirtualId({\n hasDefault: Boolean(parsed.defaultLocal),\n fonts: Array.from(new Set(fontImports.map((spec) => spec.imported))),\n utilities: Array.from(new Set(utilityImports.map((spec) => spec.imported))),\n });\n s.overwrite(\n matchStart,\n matchEnd,\n `import ${clause} from ${JSON.stringify(virtualId)};`,\n );\n overwrittenRanges.push([matchStart, matchEnd]);\n hasChanges = true;\n continue;\n }\n\n if (parsed.namespaceLocal) {\n const proxyImportName = `__vinext_google_fonts_proxy_${proxyImportCounter++}`;\n const replacementLines = [\n `import ${proxyImportName} from ${JSON.stringify(fontGoogleShimPath)};`,\n ];\n if (parsed.defaultLocal) {\n replacementLines.push(`var ${parsed.defaultLocal} = ${proxyImportName};`);\n }\n replacementLines.push(`var ${parsed.namespaceLocal} = ${proxyImportName};`);\n s.overwrite(matchStart, matchEnd, replacementLines.join(\"\\n\"));\n overwrittenRanges.push([matchStart, matchEnd]);\n proxyObjectLocals.add(parsed.namespaceLocal);\n hasChanges = true;\n }\n }\n\n const exportRe = /^[ \\t]*export\\s*\\{([^}]+)\\}\\s*from\\s*([\"'])next\\/font\\/google\\2\\s*;?/gm;\n let exportMatch;\n while ((exportMatch = exportRe.exec(code)) !== null) {\n const [fullMatch, specifiers] = exportMatch;\n const matchStart = exportMatch.index;\n const matchEnd = matchStart + fullMatch.length;\n const namedExports = parseGoogleFontNamedSpecifiers(specifiers);\n const utilityExports = namedExports.filter(\n (spec) => !spec.isType && GOOGLE_FONT_UTILITY_EXPORTS.has(spec.imported),\n );\n const fontExports = namedExports.filter(\n (spec) => !spec.isType && !GOOGLE_FONT_UTILITY_EXPORTS.has(spec.imported),\n );\n if (fontExports.length === 0) continue;\n\n const virtualId = encodeGoogleFontsVirtualId({\n hasDefault: false,\n fonts: Array.from(new Set(fontExports.map((spec) => spec.imported))),\n utilities: Array.from(new Set(utilityExports.map((spec) => spec.imported))),\n });\n s.overwrite(\n matchStart,\n matchEnd,\n `export { ${specifiers.trim()} } from ${JSON.stringify(virtualId)};`,\n );\n overwrittenRanges.push([matchStart, matchEnd]);\n hasChanges = true;\n }\n\n async function injectSelfHostedCss(\n callStart: number,\n callEnd: number,\n optionsStr: string,\n family: string,\n calleeSource: string,\n ) {\n // Parse options safely via AST — no eval/new Function\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n let options: Record<string, any> = {};\n try {\n const parsed = parseStaticObjectLiteral(optionsStr);\n if (!parsed) return; // Contains dynamic expressions, skip\n options = parsed as Record<string, unknown>;\n } catch {\n return; // Can't parse options statically, skip\n }\n\n // Validate the call against the bundled Google Fonts metadata\n // and resolve the actual axis values. This replaces an earlier\n // inline URL builder that hardcoded `:wght@100..900` regardless\n // of the font's real `wght` axis range, which produced HTTP 400\n // for fonts whose axis is narrower (Sen 400..800, Anton 400).\n // See issue #885.\n let validated;\n try {\n validated = validateGoogleFontOptions(family, options);\n } catch (err) {\n // Validation errors are programmer errors (unknown family,\n // missing required weight on a static font, etc.). Re-throw\n // with the file path attached so Vite reports the offending\n // call site instead of a generic plugin error.\n const message = err instanceof Error ? err.message : String(err);\n throw new Error(`[vinext:google-fonts] ${id}: ${message}`);\n }\n const axes = getFontAxes(\n family,\n validated.weights,\n validated.styles,\n validated.selectedVariableAxes,\n );\n const cssUrl = buildGoogleFontsUrl(family, axes, validated.display);\n\n // Check cache\n let localCSS = fontCache.get(cssUrl);\n if (!localCSS) {\n try {\n localCSS = await fetchAndCacheFont(cssUrl, family, cacheDir);\n fontCache.set(cssUrl, localCSS);\n } catch (err) {\n if (err instanceof GoogleFontsHttpError) {\n // HTTP 4xx/5xx from Google means the URL is malformed or\n // the family/axis combination is invalid. Surface as a\n // build error so the user sees the failing URL plus\n // Google's response body, rather than silently falling\n // through to a CDN URL that ships the same bad request\n // to the browser.\n throw new Error(\n `[vinext:google-fonts] ${id}: Google Fonts returned HTTP ${err.status} for ${err.url}.\\n${formatGoogleFontsErrorBody(err.responseBody)}`,\n );\n }\n // Network errors (offline, DNS, AbortError) are recoverable;\n // skip self-hosting and let the runtime CDN path handle it.\n return;\n }\n }\n\n // Rewrite absolute `.vinext/fonts/` filesystem paths in the cached\n // CSS to served URLs under `/<assetsDir>/_vinext_fonts/` so the\n // embedded `_vinext.font.selfHostedCSS` string has origin-relative URLs that\n // the browser can actually resolve. The plugin's writeBundle hook\n // copies the referenced font files to the matching location under\n // the client output directory so the URLs serve 200s, not 404s.\n //\n // `transformAssetsDir` is captured at the top of the outer\n // transform handler (where `this.environment` is bound by\n // Rollup to the plugin context) and closed over here. This\n // keeps the embedded URL prefix in lockstep with the directory\n // the writeBundle hook copies files into, so a user who\n // customizes `build.assetsDir` (e.g. to `\"static\"`) sees both\n // the CSS and the copy target move together — otherwise the\n // rewritten URLs would 404 in production.\n const servedCSS = _rewriteCachedFontCssToServedUrls(\n localCSS,\n cacheDir,\n transformAssetsDir,\n );\n const fallbackMetrics =\n validated.adjustFontFallback === false\n ? undefined\n : getFallbackFontOverrideMetrics(family);\n const adjustedFallbackCSS = fallbackMetrics\n ? buildFallbackFontFace(family, fallbackMetrics)\n : undefined;\n const validatedFontWeight =\n validated.weights.length === 1 && validated.weights[0] !== \"variable\"\n ? Number(validated.weights[0])\n : undefined;\n const validatedFontStyle =\n validated.styles.length === 1 ? validated.styles[0] : undefined;\n\n // Inject the internal transform-to-runtime payload into the options object.\n const internalFontProperties = [`selfHostedCSS: ${JSON.stringify(servedCSS)}`];\n if (adjustedFallbackCSS) {\n internalFontProperties.push(\n `adjustedFallbackCSS: ${JSON.stringify(adjustedFallbackCSS)}`,\n );\n }\n if (Number.isFinite(validatedFontWeight)) {\n internalFontProperties.push(`fontWeight: ${validatedFontWeight}`);\n }\n if (validatedFontStyle) {\n internalFontProperties.push(`fontStyle: ${JSON.stringify(validatedFontStyle)}`);\n }\n const injectedProperties = [\n `_vinext: { font: { ${internalFontProperties.join(\", \")} } }`,\n ];\n const closingBrace = optionsStr.lastIndexOf(\"}\");\n const beforeBrace = optionsStr.slice(0, closingBrace).trim();\n // Determine the separator to insert before the new property:\n // - Empty string if the object is empty ({ is the last non-whitespace char)\n // - Empty string if there's already a trailing comma (avoid double comma)\n // - \", \" otherwise (before the new property)\n const separator = beforeBrace.endsWith(\"{\") || beforeBrace.endsWith(\",\") ? \"\" : \", \";\n const optionsWithCSS =\n optionsStr.slice(0, closingBrace) +\n separator +\n injectedProperties.join(\", \") +\n optionsStr.slice(closingBrace);\n\n const replacement = `${calleeSource}(${optionsWithCSS})`;\n s.overwrite(callStart, callEnd, replacement);\n overwrittenRanges.push([callStart, callEnd]);\n hasChanges = true;\n }\n\n // Self-host injection runs in both dev and build. In dev, the\n // companion `configureServer` hook serves the cached files\n // directly from `.vinext/fonts/` so the rewritten URLs resolve\n // against the dev origin; in build, the `writeBundle` hook copies\n // them into the client output directory.\n\n // Match: Identifier( — where the argument starts with {\n // The regex intentionally does NOT capture the options object; we use\n // _findBalancedObject() to handle nested braces correctly.\n const namedCallRe = /\\b([A-Za-z_$][A-Za-z0-9_$]*)\\s*\\(\\s*(?=\\{)/g;\n let namedCallMatch;\n while ((namedCallMatch = namedCallRe.exec(code)) !== null) {\n const [fullMatch, localName] = namedCallMatch;\n const importedName = fontLocals.get(localName);\n if (!importedName) continue;\n\n const callStart = namedCallMatch.index;\n // The regex consumed up to (but not including) the '{' due to the\n // lookahead — find the balanced object starting at the lookahead pos.\n const openParenEnd = callStart + fullMatch.length;\n const objRange = _findBalancedObject(code, openParenEnd);\n if (!objRange) continue;\n const optionsStr = code.slice(objRange[0], objRange[1]);\n const callEnd = _findCallEnd(code, objRange[1]);\n if (callEnd === null) continue;\n\n if (overwrittenRanges.some(([start, end]) => callStart < end && callEnd > start)) {\n continue;\n }\n\n await injectSelfHostedCss(\n callStart,\n callEnd,\n optionsStr,\n importedName.replace(/_/g, \" \"),\n localName,\n );\n }\n\n // Match: Identifier.Identifier( — where the argument starts with {\n const memberCallRe =\n /\\b([A-Za-z_$][A-Za-z0-9_$]*)\\.([A-Za-z_$][A-Za-z0-9_$]*)\\s*\\(\\s*(?=\\{)/g;\n let memberCallMatch;\n while ((memberCallMatch = memberCallRe.exec(code)) !== null) {\n const [fullMatch, objectName, propName] = memberCallMatch;\n if (!proxyObjectLocals.has(objectName)) continue;\n\n const callStart = memberCallMatch.index;\n const openParenEnd = callStart + fullMatch.length;\n const objRange = _findBalancedObject(code, openParenEnd);\n if (!objRange) continue;\n const optionsStr = code.slice(objRange[0], objRange[1]);\n const callEnd = _findCallEnd(code, objRange[1]);\n if (callEnd === null) continue;\n\n if (overwrittenRanges.some(([start, end]) => callStart < end && callEnd > start)) {\n continue;\n }\n\n await injectSelfHostedCss(\n callStart,\n callEnd,\n optionsStr,\n propertyNameToGoogleFontFamily(propName),\n `${objectName}.${propName}`,\n );\n }\n\n if (!hasChanges) return null;\n return {\n code: s.toString(),\n map: s.generateMap({ hires: \"boundary\" }),\n };\n },\n },\n\n // Copy cached Google Font files into the client output so the served\n // URLs produced by `_rewriteCachedFontCssToServedUrls` resolve against\n // the origin. Runs once, at the end of the client environment's build.\n //\n // `fetchAndCacheFont` downloads files into `<root>/.vinext/fonts/` and\n // leaves them there — nothing else copies them. Without this hook, the\n // rewritten `/assets/_vinext_fonts/...` URLs would 404 in production.\n writeBundle: {\n sequential: true,\n order: \"post\" as const,\n handler(outputOptions: { dir?: string }) {\n // Only copy on the client build — the server/SSR environments\n // don't serve static assets.\n //\n // Optional chaining on `this.environment` matches the convention\n // used by the other build-time plugins in `src/index.ts` (the\n // `vinext:precompress` and `vinext:cloudflare-build` plugins both\n // guard on `this.environment?.name !== \"client\"`). Vite 6+ always\n // populates `this.environment` inside writeBundle, but keeping\n // the guard makes the hook safely no-op if the code is ever\n // executed in a context where Rollup invokes it without a bound\n // environment (e.g. a thin unit test harness that invokes the\n // hook directly). Concretely: under normal Vite builds this\n // always resolves, the early-return is never taken.\n if (this.environment?.name !== \"client\") return;\n if (!cacheDir || !fs.existsSync(cacheDir)) return;\n const outDir = outputOptions.dir;\n if (!outDir) return;\n\n // Read the resolved `build.assetsDir` from the same environment\n // that the transform-time rewrite read it from, so the embedded\n // URL prefix and the physical copy location cannot diverge even\n // if a user customizes `build.assetsDir`.\n const assetsDir = this.environment.config?.build?.assetsDir ?? DEFAULT_ASSETS_DIR;\n const targetRoot = path.join(outDir, assetsDir, VINEXT_FONT_URL_NAMESPACE);\n\n // Recursive copy of every cached font file. Skip the companion\n // `style.css` artifact — that is only read by the build plugin\n // itself, never served at runtime.\n const stack: string[] = [cacheDir];\n while (stack.length > 0) {\n const dir = stack.pop();\n if (!dir) continue;\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const src = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n stack.push(src);\n continue;\n }\n if (!/\\.(woff2?|ttf|otf|eot)$/i.test(entry.name)) continue;\n const relative = path.relative(cacheDir, src);\n const dest = path.join(targetRoot, relative);\n fs.mkdirSync(path.dirname(dest), { recursive: true });\n fs.copyFileSync(src, dest);\n }\n }\n },\n },\n } satisfies Plugin;\n}\n\n/**\n * Create the `vinext:local-fonts` Vite plugin.\n *\n * Rewrites relative font file paths in `next/font/local` calls into Vite\n * asset import references so that both dev (/@fs/...) and prod\n * (/assets/font-xxx.woff2) URLs resolve correctly.\n */\nexport function createLocalFontsPlugin(): Plugin {\n return {\n name: \"vinext:local-fonts\",\n enforce: \"pre\",\n\n transform: {\n filter: {\n id: {\n include: /\\.(tsx?|jsx?|mjs)$/,\n exclude: /node_modules/,\n },\n code: \"next/font/local\",\n },\n handler(code, id) {\n // Defensive guards — duplicate filter logic\n if (id.includes(\"node_modules\")) return null;\n if (id.startsWith(\"\\0\")) return null;\n if (!id.match(/\\.(tsx?|jsx?|mjs)$/)) return null;\n if (!code.includes(\"next/font/local\")) return null;\n // Skip vinext's own font-local shim — it contains example paths\n // in comments that would be incorrectly rewritten.\n if (id.includes(\"font-local\")) return null;\n\n // Verify there's actually an import from next/font/local\n const importRe = /import\\s+\\w+\\s+from\\s*['\"]next\\/font\\/local['\"]/;\n if (!importRe.test(code)) return null;\n\n const s = new MagicString(code);\n let hasChanges = false;\n let fontImportCounter = 0;\n const imports: string[] = [];\n\n // Match font file paths in `path: \"...\"` or `src: \"...\"` properties.\n // Captures: (1) property+colon prefix, (2) quote char, (3) the path.\n const fontPathRe = /((?:path|src)\\s*:\\s*)(['\"])([^'\"]+\\.(?:woff2?|ttf|otf|eot))\\2/g;\n\n let match;\n while ((match = fontPathRe.exec(code)) !== null) {\n const [fullMatch, prefix, _quote, fontPath] = match;\n const varName = `__vinext_local_font_${fontImportCounter++}`;\n\n // Add an import for this font file — Vite resolves it as a static\n // asset and returns the correct URL for both dev and prod.\n imports.push(`import ${varName} from ${JSON.stringify(fontPath)};`);\n\n // Replace: path: \"./font.woff2\" -> path: __vinext_local_font_0\n const matchStart = match.index;\n const matchEnd = matchStart + fullMatch.length;\n s.overwrite(matchStart, matchEnd, `${prefix}${varName}`);\n hasChanges = true;\n }\n\n if (!hasChanges) return null;\n\n // Prepend the asset imports at the top of the file\n s.prepend(imports.join(\"\\n\") + \"\\n\");\n\n return {\n code: s.toString(),\n map: s.generateMap({ hires: \"boundary\" }),\n };\n },\n },\n } satisfies Plugin;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"fonts.js","names":[],"sources":["../../src/plugins/fonts.ts"],"sourcesContent":["/**\n * vinext font plugins\n *\n * Exports two Vite plugins:\n *\n * `createGoogleFontsPlugin` — vinext:google-fonts\n * 1. Rewrites named `next/font/google` imports/exports to tiny virtual modules\n * that export only the requested fonts plus any utility exports. This lets us\n * delete the generated ~1,900-line runtime catalog while keeping ESM import\n * semantics intact.\n * 2. During production builds, fetches Google Fonts CSS + font files, caches\n * them locally under `.vinext/fonts/`, and injects `_vinext.font` into\n * statically analyzable font loader calls so fonts are served from the\n * deployed origin rather than fonts.googleapis.com. Static calls also\n * receive adjusted fallback CSS when Next.js-compatible fallback metrics\n * exist for the selected Google Font.\n *\n * `createLocalFontsPlugin` — vinext:local-fonts\n * When a source file calls localFont({ src: \"./font.woff2\" }) or\n * localFont({ src: [{ path: \"./font.woff2\" }] }), the relative paths\n * won't resolve in the browser because the CSS is injected at runtime.\n * This plugin rewrites those path strings into Vite asset import references\n * so that both dev (/@fs/...) and prod (/assets/font-xxx.woff2) URLs are\n * correct.\n */\n\nimport type { Plugin } from \"vite\";\nimport { parseAst } from \"vite\";\nimport path from \"node:path\";\nimport fs from \"node:fs\";\nimport MagicString from \"magic-string\";\nimport {\n buildFallbackFontFace,\n getFallbackFontOverrideMetrics,\n} from \"../build/google-fonts/fallback-metrics.js\";\nimport { validateGoogleFontOptions } from \"../build/google-fonts/validate.js\";\nimport { getFontAxes } from \"../build/google-fonts/get-axes.js\";\nimport { buildGoogleFontsUrl } from \"../build/google-fonts/build-url.js\";\nimport { CONTENT_TYPES } from \"../server/static-file-cache.js\";\nimport { ASSET_PREFIX_URL_DIR } from \"../utils/asset-prefix.js\";\n\n/**\n * Thrown when Google Fonts returns a non-2xx response. Distinct from a raw\n * `fetch` rejection (network error, DNS failure, AbortError) so the call\n * site can decide whether to surface as a build error or fall through to\n * the runtime CDN path.\n */\nclass GoogleFontsHttpError extends Error {\n constructor(\n public readonly url: string,\n public readonly status: number,\n public readonly responseBody: string,\n ) {\n super(`Google Fonts returned HTTP ${status} for ${url}`);\n this.name = \"GoogleFontsHttpError\";\n }\n}\n\n// ── Virtual module IDs ────────────────────────────────────────────────────────\n\nexport const VIRTUAL_GOOGLE_FONTS = \"virtual:vinext-google-fonts\";\nexport const RESOLVED_VIRTUAL_GOOGLE_FONTS = \"\\0\" + VIRTUAL_GOOGLE_FONTS;\n\n// ── Constants ─────────────────────────────────────────────────────────────────\n\n// IMPORTANT: keep this set in sync with the non-default exports from\n// packages/vinext/src/shims/font-google.ts (and its re-export barrel).\nconst GOOGLE_FONT_UTILITY_EXPORTS = new Set([\n \"buildGoogleFontsUrl\",\n \"getSSRFontLinks\",\n \"getSSRFontStyles\",\n \"getSSRFontPreloads\",\n \"createFontLoader\",\n]);\n\n/**\n * Served URL prefix for self-hosted Google Font files.\n *\n * `fetchAndCacheFont()` downloads .woff2 files into `<root>/.vinext/fonts/`\n * and writes an `@font-face` CSS snippet whose `src: url(...)` references\n * the files by absolute filesystem path — convenient for disk, unusable at\n * runtime because browsers resolve relative to the origin. Before the CSS\n * is embedded in the bundle as `_vinext.font.selfHostedCSS`, the filesystem\n * prefix is rewritten to this URL prefix by `_rewriteCachedFontCssToServedUrls()`,\n * and the matching `writeBundle` hook in `createGoogleFontsPlugin` copies\n * the font files into `<clientOutDir>/<assetsDir>/_vinext_fonts/` so the\n * rewritten URL actually resolves against the origin at request time.\n *\n * The leading `_` keeps the namespace distinct from Vite's content-hashed\n * asset names (which are emitted flat into `<assetsDir>/`) and from any\n * user-provided public files.\n */\nconst VINEXT_FONT_URL_NAMESPACE = \"_vinext_fonts\";\nconst MAX_GOOGLE_FONTS_ERROR_BODY_LENGTH = 500;\n\nfunction formatGoogleFontsErrorBody(body: string): string {\n const trimmed = body.trim();\n if (!trimmed) return \"(empty response body)\";\n if (trimmed.length <= MAX_GOOGLE_FONTS_ERROR_BODY_LENGTH) return trimmed;\n const omitted = trimmed.length - MAX_GOOGLE_FONTS_ERROR_BODY_LENGTH;\n return `${trimmed.slice(0, MAX_GOOGLE_FONTS_ERROR_BODY_LENGTH)}\\n... (truncated ${omitted} characters)`;\n}\n\n/**\n * Rewrite absolute filesystem paths in cached Google Fonts CSS so the\n * `@font-face { src: url(...) }` references point at the served URL the\n * plugin's `writeBundle` hook copies the font files to.\n *\n * This is called once per transform, before the CSS string is embedded in\n * the bundle as `_vinext.font.selfHostedCSS`. Every downstream consumer reads\n * from the same rewritten CSS: the injected `<style data-vinext-fonts>` block, the\n * HTML body's `<link rel=\"preload\">` tags (via `collectFontPreloadsFromCSS`\n * in `shims/font-google-base.ts`), and the HTTP `Link:` response header\n * (via `buildAppPageFontLinkHeader` in `server/app-page-execution.ts`).\n *\n * Without this rewrite, all three emit the dev-machine filesystem path\n * (e.g. `/home/user/project/.vinext/fonts/geist-<hash>/geist-<hash>.woff2`)\n * and any production request fetches `<origin>/home/user/...` → 404.\n *\n * `assetsDir` must match whatever Vite has resolved for\n * `build.assetsDir` on the client environment — otherwise the embedded\n * CSS URLs and the files emitted by the `writeBundle` hook would diverge\n * and a user who customizes `build.assetsDir` (e.g. to `\"static\"`) would\n * see 404s on every preload. The call site in `injectSelfHostedCss`\n * passes the resolved value through from plugin state. The default is\n * kept only so the exported helper can be driven directly from unit\n * tests without synthesizing a full plugin context.\n *\n * Uses split/join rather than regex because `cacheDir` is an absolute\n * filesystem path that may contain regex metacharacters on unusual\n * filesystems.\n */\nexport function _rewriteCachedFontCssToServedUrls(\n css: string,\n cacheDir: string,\n assetsDir: string = DEFAULT_ASSETS_DIR,\n): string {\n if (!cacheDir || !css.includes(cacheDir)) return css;\n const prefix = assetsDir || DEFAULT_ASSETS_DIR;\n return css.split(cacheDir).join(`/${prefix}/${VINEXT_FONT_URL_NAMESPACE}`);\n}\n\n/**\n * Default `build.assetsDir` — matches vinext's resolved default in\n * `resolveAssetsDir(\"\")` (Next.js's canonical convention). Used as the\n * fallback for the `assetsDir` parameter of\n * `_rewriteCachedFontCssToServedUrls` so the exported helper can be unit\n * tested without synthesizing plugin state. Production call sites thread\n * the real `envConfig.build.assetsDir` resolved by Vite through so that\n * the embedded CSS URLs always match the directory the `writeBundle`\n * hook copies the font files into.\n */\nconst DEFAULT_ASSETS_DIR = ASSET_PREFIX_URL_DIR;\n\n// ── Types ─────────────────────────────────────────────────────────────────────\n\ntype GoogleFontNamedSpecifier = {\n imported: string;\n local: string;\n isType: boolean;\n raw: string;\n};\n\n// ── Helpers shared with index.ts ──────────────────────────────────────────────\n\n/**\n * Safely parse a static JS object literal string into a plain object.\n * Uses Vite's parseAst (Rollup/acorn) so no code is ever evaluated.\n * Returns null if the expression contains anything dynamic (function calls,\n * template literals, identifiers, computed properties, etc.).\n *\n * Supports: string literals, numeric literals, boolean literals,\n * arrays of the above, and nested object literals.\n */\nexport function parseStaticObjectLiteral(objectStr: string): Record<string, unknown> | null {\n let ast: ReturnType<typeof parseAst>;\n try {\n // Wrap in parens so the parser treats `{…}` as an expression, not a block\n ast = parseAst(`(${objectStr})`);\n } catch {\n return null;\n }\n\n // The AST should be: Program > ExpressionStatement > ObjectExpression\n const body = ast.body;\n if (body.length !== 1 || body[0].type !== \"ExpressionStatement\") return null;\n\n const expr = body[0].expression;\n if (expr.type !== \"ObjectExpression\") return null;\n\n const result = extractStaticValue(expr);\n return result === undefined ? null : (result as Record<string, unknown>);\n}\n\n/**\n * Recursively extract a static value from an ESTree AST node.\n * Returns undefined (not null) if the node contains any dynamic expression.\n *\n * Uses `any` for the node parameter because Rollup's internal ESTree types\n * (estree.Expression, estree.ObjectExpression, etc.) aren't re-exported by Vite,\n * and the recursive traversal touches many different node shapes.\n */\n// oxlint-disable-next-line @typescript-eslint/no-explicit-any\nfunction extractStaticValue(node: any): unknown {\n switch (node.type) {\n case \"Literal\":\n // String, number, boolean, null\n return node.value;\n\n case \"UnaryExpression\":\n // Handle negative numbers: -1, -3.14\n if (\n node.operator === \"-\" &&\n node.argument?.type === \"Literal\" &&\n typeof node.argument.value === \"number\"\n ) {\n return -node.argument.value;\n }\n return undefined;\n\n case \"ArrayExpression\": {\n const arr: unknown[] = [];\n for (const elem of node.elements) {\n if (!elem) return undefined; // sparse array\n const val = extractStaticValue(elem);\n if (val === undefined) return undefined;\n arr.push(val);\n }\n return arr;\n }\n\n case \"ObjectExpression\": {\n const obj: Record<string, unknown> = {};\n for (const prop of node.properties) {\n if (prop.type !== \"Property\") return undefined; // SpreadElement etc.\n if (prop.computed) return undefined; // [expr]: val\n\n // Key can be Identifier (unquoted) or Literal (quoted)\n let key: string;\n if (prop.key.type === \"Identifier\") {\n key = prop.key.name;\n } else if (prop.key.type === \"Literal\" && typeof prop.key.value === \"string\") {\n key = prop.key.value;\n } else {\n return undefined;\n }\n\n const val = extractStaticValue(prop.value);\n if (val === undefined) return undefined;\n obj[key] = val;\n }\n return obj;\n }\n\n default:\n // TemplateLiteral, CallExpression, Identifier, etc. — reject\n return undefined;\n }\n}\n\n// ── Virtual module encoding/decoding ─────────────────────────────────────────\n\nfunction encodeGoogleFontsVirtualId(payload: {\n hasDefault: boolean;\n fonts: string[];\n utilities: string[];\n}): string {\n const params = new URLSearchParams();\n if (payload.hasDefault) params.set(\"default\", \"1\");\n if (payload.fonts.length > 0) params.set(\"fonts\", payload.fonts.join(\",\"));\n if (payload.utilities.length > 0) params.set(\"utilities\", payload.utilities.join(\",\"));\n return `${VIRTUAL_GOOGLE_FONTS}?${params.toString()}`;\n}\n\nfunction parseGoogleFontsVirtualId(id: string): {\n hasDefault: boolean;\n fonts: string[];\n utilities: string[];\n} | null {\n const cleanId = id.startsWith(\"\\0\") ? id.slice(1) : id;\n if (!cleanId.startsWith(VIRTUAL_GOOGLE_FONTS)) return null;\n const queryIndex = cleanId.indexOf(\"?\");\n const params = new URLSearchParams(queryIndex === -1 ? \"\" : cleanId.slice(queryIndex + 1));\n return {\n hasDefault: params.get(\"default\") === \"1\",\n fonts:\n params\n .get(\"fonts\")\n ?.split(\",\")\n .map((value) => value.trim())\n .filter(Boolean) ?? [],\n utilities:\n params\n .get(\"utilities\")\n ?.split(\",\")\n .map((value) => value.trim())\n .filter(Boolean) ?? [],\n };\n}\n\nexport function generateGoogleFontsVirtualModule(\n id: string,\n fontGoogleShimPath: string,\n): string | null {\n const payload = parseGoogleFontsVirtualId(id);\n if (!payload) return null;\n\n const utilities = Array.from(new Set(payload.utilities));\n const fonts = Array.from(new Set(payload.fonts));\n const lines: string[] = [];\n\n lines.push(`import { createFontLoader } from ${JSON.stringify(fontGoogleShimPath)};`);\n\n const reExports: string[] = [];\n if (payload.hasDefault) reExports.push(\"default\");\n reExports.push(...utilities);\n if (reExports.length > 0) {\n lines.push(`export { ${reExports.join(\", \")} } from ${JSON.stringify(fontGoogleShimPath)};`);\n }\n\n for (const fontName of fonts) {\n const family = fontName.replace(/_/g, \" \");\n lines.push(\n `export const ${fontName} = /*#__PURE__*/ createFontLoader(${JSON.stringify(family)});`,\n );\n }\n\n lines.push(\"\");\n return lines.join(\"\\n\");\n}\n\n// ── Import clause parsers ─────────────────────────────────────────────────────\n\nfunction parseGoogleFontNamedSpecifiers(\n specifiersStr: string,\n forceType = false,\n): GoogleFontNamedSpecifier[] {\n return specifiersStr\n .split(\",\")\n .map((spec) => spec.trim())\n .filter(Boolean)\n .map((raw) => {\n const isType = forceType || raw.startsWith(\"type \");\n const valueSpec = isType ? raw.replace(/^type\\s+/, \"\") : raw;\n const asParts = valueSpec.split(/\\s+as\\s+/);\n const imported = asParts[0]?.trim() ?? \"\";\n const local = (asParts[1] || asParts[0] || \"\").trim();\n return { imported, local, isType, raw };\n })\n .filter((spec) => spec.imported.length > 0 && spec.local.length > 0);\n}\n\nfunction parseGoogleFontImportClause(clause: string): {\n defaultLocal: string | null;\n namespaceLocal: string | null;\n named: GoogleFontNamedSpecifier[];\n} {\n const trimmed = clause.trim();\n\n if (trimmed.startsWith(\"type \")) {\n const braceStart = trimmed.indexOf(\"{\");\n const braceEnd = trimmed.lastIndexOf(\"}\");\n if (braceStart === -1 || braceEnd === -1) {\n return { defaultLocal: null, namespaceLocal: null, named: [] };\n }\n return {\n defaultLocal: null,\n namespaceLocal: null,\n named: parseGoogleFontNamedSpecifiers(trimmed.slice(braceStart + 1, braceEnd), true),\n };\n }\n\n const braceStart = trimmed.indexOf(\"{\");\n const braceEnd = trimmed.lastIndexOf(\"}\");\n if (braceStart !== -1 && braceEnd !== -1) {\n const beforeNamed = trimmed.slice(0, braceStart).trim().replace(/,\\s*$/, \"\").trim();\n return {\n defaultLocal: beforeNamed || null,\n namespaceLocal: null,\n named: parseGoogleFontNamedSpecifiers(trimmed.slice(braceStart + 1, braceEnd)),\n };\n }\n\n const commaIndex = trimmed.indexOf(\",\");\n if (commaIndex !== -1) {\n const defaultLocal = trimmed.slice(0, commaIndex).trim() || null;\n const rest = trimmed.slice(commaIndex + 1).trim();\n if (rest.startsWith(\"* as \")) {\n return {\n defaultLocal,\n namespaceLocal: rest.slice(\"* as \".length).trim() || null,\n named: [],\n };\n }\n }\n\n if (trimmed.startsWith(\"* as \")) {\n return {\n defaultLocal: null,\n namespaceLocal: trimmed.slice(\"* as \".length).trim() || null,\n named: [],\n };\n }\n\n return {\n defaultLocal: trimmed || null,\n namespaceLocal: null,\n named: [],\n };\n}\n\nfunction propertyNameToGoogleFontFamily(prop: string): string {\n return prop.replace(/_/g, \" \").replace(/([a-z])([A-Z])/g, \"$1 $2\");\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n// ── Font fetching and caching ─────────────────────────────────────────────────\n\n/**\n * Fetch Google Fonts CSS, download .woff2 files, cache locally, and return\n * @font-face CSS with local file references.\n *\n * Cache dir structure: .vinext/fonts/<family-hash>/\n * - style.css (the rewritten @font-face CSS)\n * - *.woff2 (downloaded font files)\n */\nasync function fetchAndCacheFont(\n cssUrl: string,\n family: string,\n cacheDir: string,\n): Promise<string> {\n // Use a hash of the URL for the cache key\n const { createHash } = await import(\"node:crypto\");\n const urlHash = createHash(\"md5\").update(cssUrl).digest(\"hex\").slice(0, 12);\n const fontDir = path.join(cacheDir, `${family.toLowerCase().replace(/\\s+/g, \"-\")}-${urlHash}`);\n\n // Check if already cached\n const cachedCSSPath = path.join(fontDir, \"style.css\");\n if (fs.existsSync(cachedCSSPath)) {\n return fs.readFileSync(cachedCSSPath, \"utf-8\");\n }\n\n // Fetch CSS from Google Fonts (woff2 user-agent gives woff2 URLs)\n const cssResponse = await fetch(cssUrl, {\n headers: {\n \"User-Agent\":\n \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36\",\n },\n });\n if (!cssResponse.ok) {\n // Include the response body when Google rejected the request so the\n // caller can see why (the body usually contains a one-line CSS comment\n // identifying the bad axis or family).\n const body = await cssResponse.text().catch(() => \"\");\n throw new GoogleFontsHttpError(cssUrl, cssResponse.status, body);\n }\n let css = await cssResponse.text();\n\n // Extract all font file URLs\n const urlRe = /url\\((https:\\/\\/fonts\\.gstatic\\.com\\/[^)]+)\\)/g;\n const urls = new Map<string, string>(); // original URL -> local filename\n let urlMatch;\n while ((urlMatch = urlRe.exec(css)) !== null) {\n const fontUrl = urlMatch[1];\n if (!urls.has(fontUrl)) {\n const ext = fontUrl.includes(\".woff2\")\n ? \".woff2\"\n : fontUrl.includes(\".woff\")\n ? \".woff\"\n : \".ttf\";\n const fileHash = createHash(\"md5\").update(fontUrl).digest(\"hex\").slice(0, 8);\n urls.set(fontUrl, `${family.toLowerCase().replace(/\\s+/g, \"-\")}-${fileHash}${ext}`);\n }\n }\n\n // Download font files\n fs.mkdirSync(fontDir, { recursive: true });\n for (const [fontUrl, filename] of urls) {\n const filePath = path.join(fontDir, filename);\n if (!fs.existsSync(filePath)) {\n const fontResponse = await fetch(fontUrl);\n if (fontResponse.ok) {\n const buffer = Buffer.from(await fontResponse.arrayBuffer());\n fs.writeFileSync(filePath, buffer);\n }\n }\n // Rewrite every remote Google Fonts CDN URL in the cached CSS to the\n // absolute filesystem path of the locally-downloaded font file. This\n // cache file is read back by the plugin and then run through\n // `_rewriteCachedFontCssToServedUrls()` at embed time, which replaces\n // the absolute `cacheDir` prefix with the served URL namespace under\n // `/<assetsDir>/_vinext_fonts/`. The filesystem path is only the\n // on-disk intermediate form — it must never reach the bundle, the\n // injected `<style data-vinext-fonts>` block, the HTML `<link\n // rel=\"preload\">` tags, or the HTTP `Link:` response header. An\n // earlier version of this code claimed \"Vite will resolve /@fs/ for\n // dev, or asset for build\", which was never true: the CSS is\n // embedded as a JavaScript string literal and Vite's asset pipeline\n // does not scan string literals. Do not resurrect that assumption.\n css = css.split(fontUrl).join(filePath.replaceAll(\"\\\\\", \"/\"));\n }\n\n // Cache the rewritten CSS\n fs.writeFileSync(cachedCSSPath, css);\n return css;\n}\n\n// ── Plugin factories ──────────────────────────────────────────────────────────\n\n/**\n * Create the `vinext:google-fonts` Vite plugin.\n *\n * @param fontGoogleShimPath - Absolute path to the font-google shim module\n * (either `.ts` in source or `.js` in built packages). Resolved by the caller\n * so the plugin file has no dependency on `__dirname`.\n * @param shimsDir - Absolute path to the shims directory. Used to skip shim\n * files from transform (they contain `next/font/google` references that must\n * not be rewritten).\n */\n\n/**\n * Scan `code` forward from `searchStart` for a `{...}` object literal that\n * may contain arbitrarily nested braces. Returns `[objStart, objEnd]` where\n * `code[objStart] === '{'` and `code[objEnd - 1] === '}'`, or `null` if no\n * balanced object is found.\n *\n * String literals (single-quoted, double-quoted, and backtick template\n * literals including `${...}` interpolations) are fully skipped so that brace\n * characters inside string values do not affect the depth count.\n */\nexport function _findBalancedObject(code: string, searchStart: number): [number, number] | null {\n let i = searchStart;\n // Skip leading whitespace before the opening brace\n while (\n i < code.length &&\n (code[i] === \" \" || code[i] === \"\\t\" || code[i] === \"\\n\" || code[i] === \"\\r\")\n ) {\n i++;\n }\n if (i >= code.length || code[i] !== \"{\") return null;\n const objStart = i;\n let depth = 0;\n while (i < code.length) {\n const ch = code[i];\n if (ch === '\"' || ch === \"'\") {\n // Skip a single- or double-quoted string literal, respecting backslash escapes.\n const quote = ch;\n i++;\n while (i < code.length) {\n const sc = code[i];\n if (sc === \"\\\\\") {\n i += 2; // skip escaped character\n } else if (sc === quote) {\n i++;\n break;\n } else {\n i++;\n }\n }\n } else if (ch === \"`\") {\n // Skip a template literal, including ${...} interpolation blocks.\n // We need to track brace depth inside interpolations so that a `}`\n // that closes an interpolation isn't mistaken for closing the object.\n i++; // consume the opening backtick\n while (i < code.length) {\n const tc = code[i];\n if (tc === \"\\\\\") {\n i += 2; // skip escape sequence\n } else if (tc === \"`\") {\n i++; // end of template literal\n break;\n } else if (tc === \"$\" && code[i + 1] === \"{\") {\n // Enter a ${...} interpolation: scan forward tracking nested braces.\n i += 2; // consume '${'\n let exprDepth = 1;\n while (i < code.length && exprDepth > 0) {\n const ec = code[i];\n if (ec === \"{\") {\n exprDepth++;\n i++;\n } else if (ec === \"}\") {\n exprDepth--;\n i++;\n } else if (ec === '\"' || ec === \"'\") {\n // Quoted string inside interpolation — skip it\n const q = ec;\n i++;\n while (i < code.length) {\n if (code[i] === \"\\\\\") {\n i += 2;\n } else if (code[i] === q) {\n i++;\n break;\n } else {\n i++;\n }\n }\n } else if (ec === \"`\") {\n // Nested template literal inside interpolation — skip it\n // (simple depth-1 skip; deeply nested templates are rare in font options)\n i++;\n while (i < code.length) {\n if (code[i] === \"\\\\\") {\n i += 2;\n } else if (code[i] === \"`\") {\n i++;\n break;\n } else {\n i++;\n }\n }\n } else {\n i++;\n }\n }\n } else {\n i++;\n }\n }\n } else if (ch === \"{\") {\n depth++;\n i++;\n } else if (ch === \"}\") {\n depth--;\n i++;\n if (depth === 0) return [objStart, i];\n } else {\n i++;\n }\n }\n return null; // unbalanced\n}\n\n/**\n * Given the index just past the closing `}` of an options object, skip\n * optional whitespace and return the index after the closing `)`.\n * Returns `null` if the next non-whitespace character is not `)`.\n */\nexport function _findCallEnd(code: string, objEnd: number): number | null {\n let i = objEnd;\n while (\n i < code.length &&\n (code[i] === \" \" || code[i] === \"\\t\" || code[i] === \"\\n\" || code[i] === \"\\r\")\n ) {\n i++;\n }\n if (i >= code.length || code[i] !== \")\") return null;\n return i + 1;\n}\n\nexport function createGoogleFontsPlugin(fontGoogleShimPath: string, shimsDir: string): Plugin {\n // Vite does not bind `this` to the plugin object when calling hooks, so\n // plugin state must be held in closure variables rather than as properties.\n const fontCache = new Map<string, string>(); // url -> local @font-face CSS\n let cacheDir = \"\";\n\n return {\n name: \"vinext:google-fonts\",\n enforce: \"pre\",\n\n configResolved(config) {\n cacheDir = path.join(config.root, \".vinext\", \"fonts\");\n },\n\n // Dev-mode equivalent of the production `writeBundle` copy step. In dev\n // there is no client output directory to copy files into, so the cached\n // .vinext/fonts/ tree is served directly under the same URL prefix that\n // `_rewriteCachedFontCssToServedUrls()` embeds into the @font-face CSS\n // (`/<assetsDir>/_vinext_fonts/...`). Without this hook the rewritten\n // URLs 404 — and once `_vinext.font.selfHostedCSS` is injected, the shim no longer\n // emits the fonts.googleapis.com `<link>`, so a 404 here means no\n // glyphs render at all (no CDN fallback path).\n configureServer(server) {\n if (!cacheDir) return;\n const assetsDir =\n server.environments?.client?.config?.build?.assetsDir ??\n server.config?.build?.assetsDir ??\n DEFAULT_ASSETS_DIR;\n const urlPrefix = `/${assetsDir}/${VINEXT_FONT_URL_NAMESPACE}/`;\n server.middlewares.use((req, res, next) => {\n const url = req.url;\n if (!url || !url.startsWith(urlPrefix)) return next();\n const rawPath = url.slice(urlPrefix.length).split(\"?\")[0];\n let decoded: string;\n try {\n decoded = decodeURIComponent(rawPath);\n } catch {\n return next();\n }\n const filePath = path.resolve(cacheDir, decoded);\n // Path traversal guard — `decoded` came from the URL, so refuse\n // anything that escapes the cache root (e.g. `..%2F..%2Fetc/passwd`).\n if (filePath !== cacheDir && !filePath.startsWith(cacheDir + path.sep)) {\n return next();\n }\n fs.stat(filePath, (err, stat) => {\n if (err || !stat.isFile()) return next();\n const ext = path.extname(filePath).toLowerCase();\n // CONTENT_TYPES is the same map prod-server uses, so fonts get\n // identical MIME types in dev and prod. fetchAndCacheFont only\n // ever writes .woff2/.woff/.ttf, all of which are covered.\n res.setHeader(\"Content-Type\", CONTENT_TYPES[ext] ?? \"application/octet-stream\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n fs.createReadStream(filePath).pipe(res);\n });\n });\n },\n\n transform: {\n // Hook filter: only invoke JS when code contains 'next/font/google'.\n // This still eliminates nearly all Rust-to-JS calls since very few files\n // import from next/font/google.\n filter: {\n id: {\n include: /\\.(tsx?|jsx?|mjs)$/,\n },\n code: \"next/font/google\",\n },\n async handler(code, id) {\n // Defensive guard — duplicates filter logic\n if (id.startsWith(\"\\0\")) return null;\n if (!id.match(/\\.(tsx?|jsx?|mjs)$/)) return null;\n if (!code.includes(\"next/font/google\")) return null;\n if (id.startsWith(shimsDir)) return null;\n\n // Read the resolved `build.assetsDir` from the current Vite\n // environment so it can be closed over by the inner\n // `injectSelfHostedCss` helper (a plain function declaration\n // where `this` is untyped). Captured at the top of the hook so\n // a single handler invocation always threads one consistent\n // value through every font-loader call site it rewrites.\n const transformAssetsDir = this.environment?.config?.build?.assetsDir ?? DEFAULT_ASSETS_DIR;\n\n const s = new MagicString(code);\n let hasChanges = false;\n let proxyImportCounter = 0;\n const overwrittenRanges: Array<[number, number]> = [];\n const fontLocals = new Map<string, string>();\n const proxyObjectLocals = new Set<string>();\n\n // The clause is a sequence of either a brace block (`\\{[^}]*?\\}` —\n // newlines allowed inside, but `[^}]` keeps it from spanning past\n // the matching close brace) or a single non-`;` non-`\\n` char.\n // Effect: multi-line bracket imports (Prettier wraps past\n // `printWidth`) match, but a preceding semicolon-less line\n // (e.g. `import type { Metadata } from 'next'`) can't be swallowed\n // into the clause via newline crossings. Both shapes used to fail\n // silently — the rewrite was skipped because the resulting clause\n // wasn't a valid single import.\n const importRe =\n /^[ \\t]*import\\s+((?:\\{[^}]*?\\}|[^;\\n])+?)\\s+from\\s*([\"'])next\\/font\\/google\\2\\s*;?/gm;\n let importMatch;\n while ((importMatch = importRe.exec(code)) !== null) {\n const [fullMatch, clause] = importMatch;\n const matchStart = importMatch.index;\n const matchEnd = matchStart + fullMatch.length;\n const parsed = parseGoogleFontImportClause(clause);\n const utilityImports = parsed.named.filter(\n (spec) => !spec.isType && GOOGLE_FONT_UTILITY_EXPORTS.has(spec.imported),\n );\n const fontImports = parsed.named.filter(\n (spec) => !spec.isType && !GOOGLE_FONT_UTILITY_EXPORTS.has(spec.imported),\n );\n\n if (parsed.defaultLocal) {\n proxyObjectLocals.add(parsed.defaultLocal);\n }\n for (const fontImport of fontImports) {\n fontLocals.set(fontImport.local, fontImport.imported);\n }\n\n if (fontImports.length > 0) {\n const virtualId = encodeGoogleFontsVirtualId({\n hasDefault: Boolean(parsed.defaultLocal),\n fonts: Array.from(new Set(fontImports.map((spec) => spec.imported))),\n utilities: Array.from(new Set(utilityImports.map((spec) => spec.imported))),\n });\n s.overwrite(\n matchStart,\n matchEnd,\n `import ${clause} from ${JSON.stringify(virtualId)};`,\n );\n overwrittenRanges.push([matchStart, matchEnd]);\n hasChanges = true;\n continue;\n }\n\n if (parsed.namespaceLocal) {\n const proxyImportName = `__vinext_google_fonts_proxy_${proxyImportCounter++}`;\n const replacementLines = [\n `import ${proxyImportName} from ${JSON.stringify(fontGoogleShimPath)};`,\n ];\n if (parsed.defaultLocal) {\n replacementLines.push(`var ${parsed.defaultLocal} = ${proxyImportName};`);\n }\n replacementLines.push(`var ${parsed.namespaceLocal} = ${proxyImportName};`);\n s.overwrite(matchStart, matchEnd, replacementLines.join(\"\\n\"));\n overwrittenRanges.push([matchStart, matchEnd]);\n proxyObjectLocals.add(parsed.namespaceLocal);\n hasChanges = true;\n }\n }\n\n const exportRe = /^[ \\t]*export\\s*\\{([^}]+)\\}\\s*from\\s*([\"'])next\\/font\\/google\\2\\s*;?/gm;\n let exportMatch;\n while ((exportMatch = exportRe.exec(code)) !== null) {\n const [fullMatch, specifiers] = exportMatch;\n const matchStart = exportMatch.index;\n const matchEnd = matchStart + fullMatch.length;\n const namedExports = parseGoogleFontNamedSpecifiers(specifiers);\n const utilityExports = namedExports.filter(\n (spec) => !spec.isType && GOOGLE_FONT_UTILITY_EXPORTS.has(spec.imported),\n );\n const fontExports = namedExports.filter(\n (spec) => !spec.isType && !GOOGLE_FONT_UTILITY_EXPORTS.has(spec.imported),\n );\n if (fontExports.length === 0) continue;\n\n const virtualId = encodeGoogleFontsVirtualId({\n hasDefault: false,\n fonts: Array.from(new Set(fontExports.map((spec) => spec.imported))),\n utilities: Array.from(new Set(utilityExports.map((spec) => spec.imported))),\n });\n s.overwrite(\n matchStart,\n matchEnd,\n `export { ${specifiers.trim()} } from ${JSON.stringify(virtualId)};`,\n );\n overwrittenRanges.push([matchStart, matchEnd]);\n hasChanges = true;\n }\n\n async function injectSelfHostedCss(\n callStart: number,\n callEnd: number,\n optionsStr: string,\n family: string,\n calleeSource: string,\n ) {\n // Parse options safely via AST — no eval/new Function\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n let options: Record<string, any> = {};\n try {\n const parsed = parseStaticObjectLiteral(optionsStr);\n if (!parsed) return; // Contains dynamic expressions, skip\n options = parsed as Record<string, unknown>;\n } catch {\n return; // Can't parse options statically, skip\n }\n\n // Validate the call against the bundled Google Fonts metadata\n // and resolve the actual axis values. This replaces an earlier\n // inline URL builder that hardcoded `:wght@100..900` regardless\n // of the font's real `wght` axis range, which produced HTTP 400\n // for fonts whose axis is narrower (Sen 400..800, Anton 400).\n // See issue #885.\n let validated;\n try {\n validated = validateGoogleFontOptions(family, options);\n } catch (err) {\n // Validation errors are programmer errors (unknown family,\n // missing required weight on a static font, etc.). Re-throw\n // with the file path attached so Vite reports the offending\n // call site instead of a generic plugin error.\n const message = err instanceof Error ? err.message : String(err);\n throw new Error(`[vinext:google-fonts] ${id}: ${message}`);\n }\n const axes = getFontAxes(\n family,\n validated.weights,\n validated.styles,\n validated.selectedVariableAxes,\n );\n const cssUrl = buildGoogleFontsUrl(family, axes, validated.display);\n\n // Check cache\n let localCSS = fontCache.get(cssUrl);\n if (!localCSS) {\n try {\n localCSS = await fetchAndCacheFont(cssUrl, family, cacheDir);\n fontCache.set(cssUrl, localCSS);\n } catch (err) {\n if (err instanceof GoogleFontsHttpError) {\n // HTTP 4xx/5xx from Google means the URL is malformed or\n // the family/axis combination is invalid. Surface as a\n // build error so the user sees the failing URL plus\n // Google's response body, rather than silently falling\n // through to a CDN URL that ships the same bad request\n // to the browser.\n throw new Error(\n `[vinext:google-fonts] ${id}: Google Fonts returned HTTP ${err.status} for ${err.url}.\\n${formatGoogleFontsErrorBody(err.responseBody)}`,\n );\n }\n // Network errors (offline, DNS, AbortError) are recoverable;\n // skip self-hosting and let the runtime CDN path handle it.\n return;\n }\n }\n\n // Rewrite absolute `.vinext/fonts/` filesystem paths in the cached\n // CSS to served URLs under `/<assetsDir>/_vinext_fonts/` so the\n // embedded `_vinext.font.selfHostedCSS` string has origin-relative URLs that\n // the browser can actually resolve. The plugin's writeBundle hook\n // copies the referenced font files to the matching location under\n // the client output directory so the URLs serve 200s, not 404s.\n //\n // `transformAssetsDir` is captured at the top of the outer\n // transform handler (where `this.environment` is bound by\n // Rollup to the plugin context) and closed over here. This\n // keeps the embedded URL prefix in lockstep with the directory\n // the writeBundle hook copies files into, so a user who\n // customizes `build.assetsDir` (e.g. to `\"static\"`) sees both\n // the CSS and the copy target move together — otherwise the\n // rewritten URLs would 404 in production.\n const servedCSS = _rewriteCachedFontCssToServedUrls(\n localCSS,\n cacheDir,\n transformAssetsDir,\n );\n const fallbackMetrics =\n validated.adjustFontFallback === false\n ? undefined\n : getFallbackFontOverrideMetrics(family);\n const adjustedFallbackCSS = fallbackMetrics\n ? buildFallbackFontFace(family, fallbackMetrics)\n : undefined;\n const validatedFontWeight =\n validated.weights.length === 1 && validated.weights[0] !== \"variable\"\n ? Number(validated.weights[0])\n : undefined;\n const validatedFontStyle =\n validated.styles.length === 1 ? validated.styles[0] : undefined;\n\n // Inject the internal transform-to-runtime payload into the options object.\n const internalFontProperties = [`selfHostedCSS: ${JSON.stringify(servedCSS)}`];\n if (adjustedFallbackCSS) {\n internalFontProperties.push(\n `adjustedFallbackCSS: ${JSON.stringify(adjustedFallbackCSS)}`,\n );\n }\n if (Number.isFinite(validatedFontWeight)) {\n internalFontProperties.push(`fontWeight: ${validatedFontWeight}`);\n }\n if (validatedFontStyle) {\n internalFontProperties.push(`fontStyle: ${JSON.stringify(validatedFontStyle)}`);\n }\n const injectedProperties = [\n `_vinext: { font: { ${internalFontProperties.join(\", \")} } }`,\n ];\n const closingBrace = optionsStr.lastIndexOf(\"}\");\n const beforeBrace = optionsStr.slice(0, closingBrace).trim();\n // Determine the separator to insert before the new property:\n // - Empty string if the object is empty ({ is the last non-whitespace char)\n // - Empty string if there's already a trailing comma (avoid double comma)\n // - \", \" otherwise (before the new property)\n const separator = beforeBrace.endsWith(\"{\") || beforeBrace.endsWith(\",\") ? \"\" : \", \";\n const optionsWithCSS =\n optionsStr.slice(0, closingBrace) +\n separator +\n injectedProperties.join(\", \") +\n optionsStr.slice(closingBrace);\n\n const replacement = `${calleeSource}(${optionsWithCSS})`;\n s.overwrite(callStart, callEnd, replacement);\n overwrittenRanges.push([callStart, callEnd]);\n hasChanges = true;\n }\n\n // Self-host injection runs in both dev and build. In dev, the\n // companion `configureServer` hook serves the cached files\n // directly from `.vinext/fonts/` so the rewritten URLs resolve\n // against the dev origin; in build, the `writeBundle` hook copies\n // them into the client output directory.\n\n // Match: Identifier( — where the argument starts with {\n // The regex intentionally does NOT capture the options object; we use\n // _findBalancedObject() to handle nested braces correctly.\n const namedCallRe = /\\b([A-Za-z_$][A-Za-z0-9_$]*)\\s*\\(\\s*(?=\\{)/g;\n let namedCallMatch;\n while ((namedCallMatch = namedCallRe.exec(code)) !== null) {\n const [fullMatch, localName] = namedCallMatch;\n const importedName = fontLocals.get(localName);\n if (!importedName) continue;\n\n const callStart = namedCallMatch.index;\n // The regex consumed up to (but not including) the '{' due to the\n // lookahead — find the balanced object starting at the lookahead pos.\n const openParenEnd = callStart + fullMatch.length;\n const objRange = _findBalancedObject(code, openParenEnd);\n if (!objRange) continue;\n const optionsStr = code.slice(objRange[0], objRange[1]);\n const callEnd = _findCallEnd(code, objRange[1]);\n if (callEnd === null) continue;\n\n if (overwrittenRanges.some(([start, end]) => callStart < end && callEnd > start)) {\n continue;\n }\n\n await injectSelfHostedCss(\n callStart,\n callEnd,\n optionsStr,\n importedName.replace(/_/g, \" \"),\n localName,\n );\n }\n\n // Match: Identifier.Identifier( — where the argument starts with {\n const memberCallRe =\n /\\b([A-Za-z_$][A-Za-z0-9_$]*)\\.([A-Za-z_$][A-Za-z0-9_$]*)\\s*\\(\\s*(?=\\{)/g;\n let memberCallMatch;\n while ((memberCallMatch = memberCallRe.exec(code)) !== null) {\n const [fullMatch, objectName, propName] = memberCallMatch;\n if (!proxyObjectLocals.has(objectName)) continue;\n\n const callStart = memberCallMatch.index;\n const openParenEnd = callStart + fullMatch.length;\n const objRange = _findBalancedObject(code, openParenEnd);\n if (!objRange) continue;\n const optionsStr = code.slice(objRange[0], objRange[1]);\n const callEnd = _findCallEnd(code, objRange[1]);\n if (callEnd === null) continue;\n\n if (overwrittenRanges.some(([start, end]) => callStart < end && callEnd > start)) {\n continue;\n }\n\n await injectSelfHostedCss(\n callStart,\n callEnd,\n optionsStr,\n propertyNameToGoogleFontFamily(propName),\n `${objectName}.${propName}`,\n );\n }\n\n if (!hasChanges) return null;\n return {\n code: s.toString(),\n map: s.generateMap({ hires: \"boundary\" }),\n };\n },\n },\n\n // Copy cached Google Font files into the client output so the served\n // URLs produced by `_rewriteCachedFontCssToServedUrls` resolve against\n // the origin. Runs once, at the end of the client environment's build.\n //\n // `fetchAndCacheFont` downloads files into `<root>/.vinext/fonts/` and\n // leaves them there — nothing else copies them. Without this hook, the\n // rewritten `/assets/_vinext_fonts/...` URLs would 404 in production.\n writeBundle: {\n sequential: true,\n order: \"post\" as const,\n handler(outputOptions: { dir?: string }) {\n // Only copy on the client build — the server/SSR environments\n // don't serve static assets.\n //\n // Optional chaining on `this.environment` matches the convention\n // used by the other build-time plugins in `src/index.ts` (the\n // `vinext:precompress` and `vinext:cloudflare-build` plugins both\n // guard on `this.environment?.name !== \"client\"`). Vite 6+ always\n // populates `this.environment` inside writeBundle, but keeping\n // the guard makes the hook safely no-op if the code is ever\n // executed in a context where Rollup invokes it without a bound\n // environment (e.g. a thin unit test harness that invokes the\n // hook directly). Concretely: under normal Vite builds this\n // always resolves, the early-return is never taken.\n if (this.environment?.name !== \"client\") return;\n if (!cacheDir || !fs.existsSync(cacheDir)) return;\n const outDir = outputOptions.dir;\n if (!outDir) return;\n\n // Read the resolved `build.assetsDir` from the same environment\n // that the transform-time rewrite read it from, so the embedded\n // URL prefix and the physical copy location cannot diverge even\n // if a user customizes `build.assetsDir`.\n const assetsDir = this.environment.config?.build?.assetsDir ?? DEFAULT_ASSETS_DIR;\n const targetRoot = path.join(outDir, assetsDir, VINEXT_FONT_URL_NAMESPACE);\n\n // Recursive copy of every cached font file. Skip the companion\n // `style.css` artifact — that is only read by the build plugin\n // itself, never served at runtime.\n const stack: string[] = [cacheDir];\n while (stack.length > 0) {\n const dir = stack.pop();\n if (!dir) continue;\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const src = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n stack.push(src);\n continue;\n }\n if (!/\\.(woff2?|ttf|otf|eot)$/i.test(entry.name)) continue;\n const relative = path.relative(cacheDir, src);\n const dest = path.join(targetRoot, relative);\n fs.mkdirSync(path.dirname(dest), { recursive: true });\n fs.copyFileSync(src, dest);\n }\n }\n },\n },\n } satisfies Plugin;\n}\n\n/**\n * Create the `vinext:local-fonts` Vite plugin.\n *\n * Rewrites relative font file paths in `next/font/local` calls into Vite\n * asset import references so that both dev (/@fs/...) and prod\n * (/assets/font-xxx.woff2) URLs resolve correctly.\n */\nexport function createLocalFontsPlugin(): Plugin {\n return {\n name: \"vinext:local-fonts\",\n enforce: \"pre\",\n\n transform: {\n filter: {\n id: {\n include: /\\.(tsx?|jsx?|mjs)$/,\n exclude: /node_modules/,\n },\n code: \"next/font/local\",\n },\n handler(code, id) {\n // Defensive guards — duplicate filter logic\n if (id.includes(\"node_modules\")) return null;\n if (id.startsWith(\"\\0\")) return null;\n if (!id.match(/\\.(tsx?|jsx?|mjs)$/)) return null;\n if (!code.includes(\"next/font/local\")) return null;\n // Skip vinext's own font-local shim — it contains example paths\n // in comments that would be incorrectly rewritten.\n if (id.includes(\"font-local\")) return null;\n\n // Verify there's actually a default import from next/font/local and\n // remember its local binding so family payloads only attach to real\n // localFont calls.\n const importMatch =\n /\\bimport\\s+([A-Za-z_$][A-Za-z0-9_$]*)\\s+from\\s*(['\"])next\\/font\\/local\\2/.exec(code);\n if (!importMatch) return null;\n const localFontIdentifier = importMatch[1];\n\n const s = new MagicString(code);\n let hasChanges = false;\n let fontImportCounter = 0;\n const imports: string[] = [];\n\n // Match font file paths in `path: \"...\"` or `src: \"...\"` properties.\n // Captures: (1) property+colon prefix, (2) quote char, (3) the path.\n const fontPathRe = /((?:path|src)\\s*:\\s*)(['\"])([^'\"]+\\.(?:woff2?|ttf|otf|eot))\\2/g;\n\n let match;\n while ((match = fontPathRe.exec(code)) !== null) {\n const [fullMatch, prefix, _quote, fontPath] = match;\n const varName = `__vinext_local_font_${fontImportCounter++}`;\n\n // Add an import for this font file — Vite resolves it as a static\n // asset and returns the correct URL for both dev and prod.\n imports.push(`import ${varName} from ${JSON.stringify(fontPath)};`);\n\n // Replace: path: \"./font.woff2\" -> path: __vinext_local_font_0\n const matchStart = match.index;\n const matchEnd = matchStart + fullMatch.length;\n s.overwrite(matchStart, matchEnd, `${prefix}${varName}`);\n hasChanges = true;\n }\n\n const localFontCallRe = new RegExp(\n String.raw`(?:^|[;{}\\n])\\s*(?:export\\s+)?(?:const|let|var)\\s+([A-Za-z_$][A-Za-z0-9_$]*)\\s*(?::[^=]+)?=\\s*${escapeRegExp(localFontIdentifier)}\\s*\\(\\s*(?=\\{)`,\n \"g\",\n );\n const familyPayloadInsertions = new Set<number>();\n let localFontCallMatch;\n while ((localFontCallMatch = localFontCallRe.exec(code)) !== null) {\n const bindingName = localFontCallMatch[1];\n const objRange = _findBalancedObject(code, localFontCallRe.lastIndex);\n if (!objRange) continue;\n if (_findCallEnd(code, objRange[1]) === null) continue;\n\n const insertAt = objRange[1] - 1;\n if (familyPayloadInsertions.has(insertAt)) continue;\n const optionsStr = code.slice(objRange[0], objRange[1]);\n if (/(?:^|[,{])\\s*_vinext\\s*:/.test(optionsStr)) continue;\n\n const beforeClosingBrace = optionsStr.slice(0, -1).trim();\n const separator =\n beforeClosingBrace.endsWith(\"{\") || beforeClosingBrace.endsWith(\",\") ? \"\" : \", \";\n s.appendLeft(\n insertAt,\n `${separator}_vinext: { font: { family: ${JSON.stringify(bindingName)} } }`,\n );\n familyPayloadInsertions.add(insertAt);\n hasChanges = true;\n }\n\n if (!hasChanges) return null;\n\n // Prepend the asset imports at the top of the file\n if (imports.length > 0) {\n s.prepend(imports.join(\"\\n\") + \"\\n\");\n }\n\n return {\n code: s.toString(),\n map: s.generateMap({ hires: \"boundary\" }),\n };\n },\n },\n } satisfies Plugin;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/plugins/remove-console.d.ts
|
|
2
|
+
type RemoveConsoleConfig = boolean | {
|
|
3
|
+
exclude: string[];
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Walk the AST body looking for expression statements whose expression is a
|
|
7
|
+
* CallExpression with a callee of `console.<identifier>`. When found, check
|
|
8
|
+
* that the name is not in the excluded set and that `console` is not shadowed
|
|
9
|
+
* at this scope. If all conditions pass, replace the entire statement with `;`.
|
|
10
|
+
*
|
|
11
|
+
* Returns `null` if no console calls are removed.
|
|
12
|
+
*/
|
|
13
|
+
declare function removeConsoleCalls(code: string, config: RemoveConsoleConfig): string | null;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { removeConsoleCalls };
|
|
16
|
+
//# sourceMappingURL=remove-console.d.ts.map
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { parseAst } from "vite";
|
|
2
|
+
import MagicString from "magic-string";
|
|
3
|
+
//#region src/plugins/remove-console.ts
|
|
4
|
+
/**
|
|
5
|
+
* Strip `console.*` calls from client bundle code.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors Next.js's SWC `remove_console` transform:
|
|
8
|
+
* - Strips all `console.<method>()` calls regardless of context (top-level,
|
|
9
|
+
* JSX expressions, function arguments, return values, ternary branches)
|
|
10
|
+
* - Replaces removed calls with `void 0` to keep the AST valid in every
|
|
11
|
+
* position a CallExpression can appear
|
|
12
|
+
* - Respects `exclude: ["error"]` to preserve certain methods (case-insensitive)
|
|
13
|
+
* - Preserves calls when `console` is shadowed (local variable, function
|
|
14
|
+
* parameter, or destructured binding)
|
|
15
|
+
* - Preserves computed property access `console[prop]()`
|
|
16
|
+
*
|
|
17
|
+
* Uses Vite's `parseAst` (OXC/acorn) for parsing and `MagicString` for
|
|
18
|
+
* surgical source replacement. Returns `null` when no changes are made.
|
|
19
|
+
*/
|
|
20
|
+
const SCOPE_ENTERING_TYPES = new Set([
|
|
21
|
+
"FunctionDeclaration",
|
|
22
|
+
"FunctionExpression",
|
|
23
|
+
"ArrowFunctionExpression"
|
|
24
|
+
]);
|
|
25
|
+
/**
|
|
26
|
+
* Walk the AST body looking for expression statements whose expression is a
|
|
27
|
+
* CallExpression with a callee of `console.<identifier>`. When found, check
|
|
28
|
+
* that the name is not in the excluded set and that `console` is not shadowed
|
|
29
|
+
* at this scope. If all conditions pass, replace the entire statement with `;`.
|
|
30
|
+
*
|
|
31
|
+
* Returns `null` if no console calls are removed.
|
|
32
|
+
*/
|
|
33
|
+
function removeConsoleCalls(code, config) {
|
|
34
|
+
if (config === false) return null;
|
|
35
|
+
const excluded = typeof config === "object" ? new Set(config.exclude.map((s) => s.toLowerCase())) : /* @__PURE__ */ new Set();
|
|
36
|
+
if (!code.match(/\bconsole\b/)) return null;
|
|
37
|
+
let ast;
|
|
38
|
+
try {
|
|
39
|
+
ast = parseAst(code);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const scopeStack = [{ shadowed: false }];
|
|
44
|
+
function currentScope() {
|
|
45
|
+
return scopeStack[scopeStack.length - 1];
|
|
46
|
+
}
|
|
47
|
+
function pushScope() {
|
|
48
|
+
scopeStack.push({ shadowed: currentScope().shadowed });
|
|
49
|
+
}
|
|
50
|
+
function popScope() {
|
|
51
|
+
scopeStack.pop();
|
|
52
|
+
}
|
|
53
|
+
const s = new MagicString(code);
|
|
54
|
+
let changed = false;
|
|
55
|
+
/**
|
|
56
|
+
* Check if a node introduces a binding named "console" and mark scope.
|
|
57
|
+
* Recurses into all binding-pattern node types (destructuring shapes,
|
|
58
|
+
* defaults, rest elements).
|
|
59
|
+
*/
|
|
60
|
+
function checkBinding(node) {
|
|
61
|
+
if (!node) return;
|
|
62
|
+
switch (node.type) {
|
|
63
|
+
case "Identifier":
|
|
64
|
+
if (node.name === "console") currentScope().shadowed = true;
|
|
65
|
+
break;
|
|
66
|
+
case "ObjectPattern":
|
|
67
|
+
for (const prop of node.properties) checkBinding(prop);
|
|
68
|
+
break;
|
|
69
|
+
case "Property": {
|
|
70
|
+
const propertyNode = node;
|
|
71
|
+
if (propertyNode.value) checkBinding(propertyNode.value);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case "ArrayPattern": {
|
|
75
|
+
const elements = node.elements;
|
|
76
|
+
if (elements) for (const el of elements) checkBinding(el);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case "AssignmentPattern": {
|
|
80
|
+
const left = node.left;
|
|
81
|
+
checkBinding(left);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case "RestElement": {
|
|
85
|
+
const argument = node.argument;
|
|
86
|
+
checkBinding(argument);
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
default: break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Check if an identifier refers to the *global* console.
|
|
94
|
+
* It's global only when:
|
|
95
|
+
* 1. Its name is "console"
|
|
96
|
+
* 2. No local binding of that name shadows it at this point
|
|
97
|
+
*/
|
|
98
|
+
function isGlobalConsole(node) {
|
|
99
|
+
return node.type === "Identifier" && node.name === "console" && !currentScope().shadowed;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Determine if a call expression is a `console.<method>()` that should be
|
|
103
|
+
* removed. The callee must be a MemberExpression with:
|
|
104
|
+
* - object: Identifier "console" (global, not shadowed)
|
|
105
|
+
* - property: Identifier (NOT computed — computed access like
|
|
106
|
+
* `console[prop]()` is preserved per Next.js behavior)
|
|
107
|
+
* The method name must NOT be in the excluded set.
|
|
108
|
+
*/
|
|
109
|
+
function shouldRemove(node) {
|
|
110
|
+
if (node.type !== "CallExpression") return false;
|
|
111
|
+
const callee = node.callee;
|
|
112
|
+
if (callee.type !== "MemberExpression") return false;
|
|
113
|
+
if (callee.computed) return false;
|
|
114
|
+
if (!isGlobalConsole(callee.object)) return false;
|
|
115
|
+
const prop = callee.property;
|
|
116
|
+
if (prop.type !== "Identifier") return false;
|
|
117
|
+
const method = prop.name.toLowerCase();
|
|
118
|
+
if (excluded.has(method)) return false;
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Recursively walk a node tree, managing scope for shadow detection.
|
|
123
|
+
* When a CallExpression matches `console.<method>()`, replace it with
|
|
124
|
+
* `void 0`. If the call is the sole expression of an ExpressionStatement,
|
|
125
|
+
* replace the entire statement with `;` for cleaner output.
|
|
126
|
+
*/
|
|
127
|
+
function walk(node, parent) {
|
|
128
|
+
if (!node) return;
|
|
129
|
+
if (node.type === "CallExpression" && shouldRemove(node)) {
|
|
130
|
+
if (parent?.type === "ExpressionStatement") s.overwrite(parent.start, parent.end, ";");
|
|
131
|
+
else s.overwrite(node.start, node.end, "void 0");
|
|
132
|
+
changed = true;
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (node.type === "FunctionDeclaration" || node.type === "ClassDeclaration") {
|
|
136
|
+
const id = node.id;
|
|
137
|
+
checkBinding(id);
|
|
138
|
+
}
|
|
139
|
+
const isScopeEntering = SCOPE_ENTERING_TYPES.has(node.type);
|
|
140
|
+
if (isScopeEntering) {
|
|
141
|
+
pushScope();
|
|
142
|
+
if (node.type === "FunctionExpression") {
|
|
143
|
+
const id = node.id;
|
|
144
|
+
checkBinding(id);
|
|
145
|
+
}
|
|
146
|
+
const params = node.params;
|
|
147
|
+
if (params) for (const param of params) checkBinding(param);
|
|
148
|
+
}
|
|
149
|
+
const isCatchScope = node.type === "CatchClause";
|
|
150
|
+
if (isCatchScope) {
|
|
151
|
+
pushScope();
|
|
152
|
+
const param = node.param;
|
|
153
|
+
if (param) checkBinding(param);
|
|
154
|
+
}
|
|
155
|
+
const isBlockScope = node.type === "BlockStatement" || node.type === "Program" || node.type === "SwitchCase";
|
|
156
|
+
if (isBlockScope && !isScopeEntering) pushScope();
|
|
157
|
+
if (node.type === "VariableDeclaration") for (const decl of node.declarations ?? []) checkBinding(decl.id);
|
|
158
|
+
for (const key of Object.keys(node)) {
|
|
159
|
+
if (key === "parent" || key === "start" || key === "end") continue;
|
|
160
|
+
const child = node[key];
|
|
161
|
+
if (child && typeof child === "object") {
|
|
162
|
+
if (Array.isArray(child)) {
|
|
163
|
+
for (const item of child) if (item && typeof item === "object" && "type" in item) walk(item, node);
|
|
164
|
+
} else if ("type" in child) walk(child, node);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (isScopeEntering || isBlockScope || isCatchScope) popScope();
|
|
168
|
+
}
|
|
169
|
+
for (const node of ast.body) walk(node);
|
|
170
|
+
if (!changed) return null;
|
|
171
|
+
return s.toString();
|
|
172
|
+
}
|
|
173
|
+
//#endregion
|
|
174
|
+
export { removeConsoleCalls };
|
|
175
|
+
|
|
176
|
+
//# sourceMappingURL=remove-console.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remove-console.js","names":[],"sources":["../../src/plugins/remove-console.ts"],"sourcesContent":["/**\n * Strip `console.*` calls from client bundle code.\n *\n * Mirrors Next.js's SWC `remove_console` transform:\n * - Strips all `console.<method>()` calls regardless of context (top-level,\n * JSX expressions, function arguments, return values, ternary branches)\n * - Replaces removed calls with `void 0` to keep the AST valid in every\n * position a CallExpression can appear\n * - Respects `exclude: [\"error\"]` to preserve certain methods (case-insensitive)\n * - Preserves calls when `console` is shadowed (local variable, function\n * parameter, or destructured binding)\n * - Preserves computed property access `console[prop]()`\n *\n * Uses Vite's `parseAst` (OXC/acorn) for parsing and `MagicString` for\n * surgical source replacement. Returns `null` when no changes are made.\n */\nimport { parseAst } from \"vite\";\nimport MagicString from \"magic-string\";\n\ntype ASTNode = ReturnType<typeof parseAst>[\"body\"][number][\"parent\"];\ntype BindingNode = Extract<ASTNode, { type: string }>;\n\ntype RemoveConsoleConfig = boolean | { exclude: string[] };\n\n// Node types that introduce a new function-style scope. Hoisted to a module-\n// level Set so the membership check in walk() is O(1) and doesn't allocate\n// per recursive call.\nconst SCOPE_ENTERING_TYPES = new Set([\n \"FunctionDeclaration\",\n \"FunctionExpression\",\n \"ArrowFunctionExpression\",\n]);\n\n/**\n * Walk the AST body looking for expression statements whose expression is a\n * CallExpression with a callee of `console.<identifier>`. When found, check\n * that the name is not in the excluded set and that `console` is not shadowed\n * at this scope. If all conditions pass, replace the entire statement with `;`.\n *\n * Returns `null` if no console calls are removed.\n */\nexport function removeConsoleCalls(code: string, config: RemoveConsoleConfig): string | null {\n if (config === false) return null;\n\n const excluded =\n typeof config === \"object\"\n ? new Set(config.exclude.map((s) => s.toLowerCase()))\n : new Set<string>();\n\n // Fast path: if there's no bare \"console\" reference, skip parsing.\n // This avoids the parse cost for the vast majority of modules.\n const consoleMatch = code.match(/\\bconsole\\b/);\n if (!consoleMatch) return null;\n\n let ast: ReturnType<typeof parseAst>;\n try {\n ast = parseAst(code);\n } catch {\n // If parsing fails (shouldn't happen post-transform), bail out\n return null;\n }\n\n // Collect shadowing scopes: tracks whether there's a local binding named\n // \"console\" in the current or any parent scope. We do a simple top-down\n // walk maintaining a stack of scope frames. Each function/arrow/block\n // introduces a new frame. When we see a declaration/parameter of name\n // \"console\", that frame (and its descendants) are shadowed.\n type Scope = { shadowed: boolean };\n const scopeStack: Scope[] = [{ shadowed: false }];\n\n function currentScope(): Scope {\n return scopeStack[scopeStack.length - 1]!;\n }\n\n function pushScope(): void {\n // Child inherits parent shadowed status\n scopeStack.push({ shadowed: currentScope().shadowed });\n }\n\n function popScope(): void {\n scopeStack.pop();\n }\n\n const s = new MagicString(code);\n let changed = false;\n\n /**\n * Check if a node introduces a binding named \"console\" and mark scope.\n * Recurses into all binding-pattern node types (destructuring shapes,\n * defaults, rest elements).\n */\n function checkBinding(node: BindingNode | null | undefined): void {\n if (!node) return;\n // oxlint-disable-next-line typescript/switch-exhaustiveness-check\n switch (node.type) {\n case \"Identifier\": {\n if (node.name === \"console\") {\n currentScope().shadowed = true;\n }\n break;\n }\n case \"ObjectPattern\": {\n for (const prop of node.properties) {\n checkBinding(prop as BindingNode);\n }\n break;\n }\n case \"Property\": {\n // For `{ key: value }` and `{ console }` (shorthand) — the binding name\n // is in `value`, not `key`. Recurse so AssignmentPattern defaults,\n // nested patterns, etc. are all handled.\n const propertyNode = node as unknown as { value?: BindingNode };\n if (propertyNode.value) checkBinding(propertyNode.value);\n break;\n }\n case \"ArrayPattern\": {\n // oxlint-disable-next-line typescript/no-explicit-any\n const elements = (node as any).elements as Array<BindingNode | null> | undefined;\n if (elements) {\n for (const el of elements) {\n checkBinding(el);\n }\n }\n break;\n }\n case \"AssignmentPattern\": {\n // `x = default` — the binding is `left`, the default value is `right`.\n // oxlint-disable-next-line typescript/no-explicit-any\n const left = (node as any).left as BindingNode | undefined;\n checkBinding(left);\n break;\n }\n case \"RestElement\": {\n // `...x` — the binding is `argument`.\n // oxlint-disable-next-line typescript/no-explicit-any\n const argument = (node as any).argument as BindingNode | undefined;\n checkBinding(argument);\n break;\n }\n default:\n break;\n }\n }\n\n /**\n * Check if an identifier refers to the *global* console.\n * It's global only when:\n * 1. Its name is \"console\"\n * 2. No local binding of that name shadows it at this point\n */\n function isGlobalConsole(node: BindingNode): boolean {\n return node.type === \"Identifier\" && node.name === \"console\" && !currentScope().shadowed;\n }\n\n /**\n * Determine if a call expression is a `console.<method>()` that should be\n * removed. The callee must be a MemberExpression with:\n * - object: Identifier \"console\" (global, not shadowed)\n * - property: Identifier (NOT computed — computed access like\n * `console[prop]()` is preserved per Next.js behavior)\n * The method name must NOT be in the excluded set.\n */\n function shouldRemove(node: BindingNode): boolean {\n if (node.type !== \"CallExpression\") return false;\n const callee = node.callee;\n if (callee.type !== \"MemberExpression\") return false;\n\n // Only handle dot access: console.log() — skip computed: console[prop]()\n if (callee.computed) return false;\n\n // The object must be the global console identifier\n if (!isGlobalConsole(callee.object)) return false;\n\n // Property must be an identifier (e.g., \"log\", \"warn\")\n const prop = callee.property;\n if (prop.type !== \"Identifier\") return false;\n\n const method = prop.name.toLowerCase();\n if (excluded.has(method)) return false;\n\n return true;\n }\n\n /**\n * Recursively walk a node tree, managing scope for shadow detection.\n * When a CallExpression matches `console.<method>()`, replace it with\n * `void 0`. If the call is the sole expression of an ExpressionStatement,\n * replace the entire statement with `;` for cleaner output.\n */\n function walk(node: ASTNode, parent?: ASTNode): void {\n if (!node) return;\n\n if (node.type === \"CallExpression\" && shouldRemove(node)) {\n if (parent?.type === \"ExpressionStatement\") {\n // Replace the whole statement so we don't leave `void 0;` litter\n s.overwrite(parent.start, parent.end, \";\");\n } else {\n s.overwrite(node.start, node.end, \"void 0\");\n }\n changed = true;\n return; // don't recurse into children of a removed call\n }\n\n // `function console() {}` and `class console {}` bind `console` in the\n // *enclosing* scope (function declarations are hoisted to function/module\n // scope, class declarations are block-scoped). Check `id` against the\n // current scope BEFORE pushing the function's own scope, so the binding\n // is visible both outside and inside the function (via scope inheritance).\n if (node.type === \"FunctionDeclaration\" || node.type === \"ClassDeclaration\") {\n // oxlint-disable-next-line typescript/no-explicit-any\n const id = (node as any).id as BindingNode | undefined;\n checkBinding(id);\n }\n\n const isScopeEntering = SCOPE_ENTERING_TYPES.has(node.type);\n if (isScopeEntering) {\n pushScope();\n\n // Named FunctionExpressions (`const x = function console() {}`) bind\n // their name only in their *own* scope — not the enclosing one — so\n // mark after pushing.\n if (node.type === \"FunctionExpression\") {\n // oxlint-disable-next-line typescript/no-explicit-any\n const id = (node as any).id as BindingNode | undefined;\n checkBinding(id);\n }\n\n // Mark params/destructured params\n // oxlint-disable-next-line typescript/no-explicit-any\n const params = (node as any).params as ASTNode[] | undefined;\n if (params) {\n for (const param of params) {\n checkBinding(param as BindingNode);\n }\n }\n }\n\n // CatchClause introduces a binding for its param (`catch (e) { ... }`).\n // If the param shadows \"console\", the catch block must treat console as\n // local. We push a scope just for the param binding; the BlockStatement\n // body below will push its own scope on top, which inherits the shadow.\n const isCatchScope = node.type === \"CatchClause\";\n if (isCatchScope) {\n pushScope();\n // oxlint-disable-next-line typescript/no-explicit-any\n const param = (node as any).param as BindingNode | undefined;\n if (param) {\n checkBinding(param);\n }\n }\n\n // Also enter a new scope for BlockStatement / Program bodies so that\n // variable declarations are checked in the right frame.\n //\n // KNOWN LIMITATION: `var` is function-scoped, not block-scoped. A `var\n // console = ...` inside a nested block (if/for/etc.) is hoisted to the\n // enclosing function, but we treat it as block-scoped here, so console\n // references after the block exits its scope frame will be incorrectly\n // stripped. `let`/`const` are block-scoped so this only affects `var`.\n // In real-world code, shadowing `console` with `var` is exceedingly rare.\n const isBlockScope =\n node.type === \"BlockStatement\" || node.type === \"Program\" || node.type === \"SwitchCase\";\n if (isBlockScope && !isScopeEntering) {\n pushScope();\n }\n\n // Check for local variable declarations of \"console\"\n if (node.type === \"VariableDeclaration\") {\n // oxlint-disable-next-line typescript/no-explicit-any\n for (const decl of (node as any).declarations ?? []) {\n checkBinding(decl.id as BindingNode);\n }\n }\n\n // Recurse into child nodes, passing current node as parent\n for (const key of Object.keys(node)) {\n if (key === \"parent\" || key === \"start\" || key === \"end\") continue;\n // oxlint-disable-next-line typescript/no-explicit-any\n const child = (node as any)[key];\n if (child && typeof child === \"object\") {\n if (Array.isArray(child)) {\n for (const item of child) {\n if (item && typeof item === \"object\" && \"type\" in item) {\n walk(item as BindingNode, node);\n }\n }\n } else if (\"type\" in child) {\n walk(child as BindingNode, node);\n }\n }\n }\n\n if (isScopeEntering || isBlockScope || isCatchScope) {\n popScope();\n }\n }\n\n for (const node of ast.body) {\n walk(node);\n }\n\n if (!changed) return null;\n return s.toString();\n}\n"],"mappings":"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"}
|