@wular/pnext 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +153 -0
- package/bin/pnext +67 -0
- package/config/lint/base.js +48 -0
- package/config/ts/base.json +26 -0
- package/config/ts/react.json +13 -0
- package/package.json +70 -0
- package/reference/compat.md +63 -0
- package/reference/config.md +120 -0
- package/reference/css.md +58 -0
- package/reference/dev.md +69 -0
- package/reference/env.md +40 -0
- package/reference/metadata.md +86 -0
- package/reference/navigation.md +149 -0
- package/reference/overview.md +35 -0
- package/reference/performance.md +97 -0
- package/reference/rendering.md +127 -0
- package/reference/routing.md +167 -0
- package/reference/typegen.md +64 -0
- package/src/api/cache.ts +80 -0
- package/src/api/client-cache.ts +9 -0
- package/src/api/client-navigation.ts +279 -0
- package/src/api/dynamic.tsx +102 -0
- package/src/api/link.tsx +119 -0
- package/src/api/navigation.ts +198 -0
- package/src/api/router/events.ts +53 -0
- package/src/api/router/history.ts +70 -0
- package/src/api/router/hub.ts +194 -0
- package/src/api/router/policies.ts +107 -0
- package/src/api/router/runtime.ts +5238 -0
- package/src/api/router/types.ts +299 -0
- package/src/api/router.ts +167 -0
- package/src/api/server.ts +323 -0
- package/src/api/suspense.ts +16 -0
- package/src/cache/context.ts +61 -0
- package/src/cli/adapters/vercel-warm.ts +375 -0
- package/src/cli/adapters/vercel.ts +1310 -0
- package/src/cli/analyze-print.ts +181 -0
- package/src/cli/analyze.ts +328 -0
- package/src/cli/boot-trace.ts +29 -0
- package/src/cli/build.ts +3114 -0
- package/src/cli/dev.ts +276 -0
- package/src/cli/index.ts +196 -0
- package/src/cli/named-bin.ts +119 -0
- package/src/cli/serve-ui.ts +160 -0
- package/src/cli/start.ts +1425 -0
- package/src/client/build.ts +2136 -0
- package/src/client/chunk-fold.ts +526 -0
- package/src/client/entry.ts +1525 -0
- package/src/client/paths.ts +22 -0
- package/src/client/prebuilt.ts +621 -0
- package/src/client/profile.ts +75 -0
- package/src/client/react-compiler.ts +94 -0
- package/src/client/reference-stub.ts +145 -0
- package/src/client/reference.ts +47 -0
- package/src/compat/actions/action-client.ts +531 -0
- package/src/compat/actions/action-dispatch.ts +676 -0
- package/src/compat/actions/action-router.ts +40 -0
- package/src/compat/actions/action-shared.ts +95 -0
- package/src/compat/actions/client-plugin.ts +135 -0
- package/src/compat/actions/client-stub.ts +65 -0
- package/src/compat/actions/config.ts +164 -0
- package/src/compat/actions/detect.ts +208 -0
- package/src/compat/actions/discovery.ts +244 -0
- package/src/compat/actions/early-submit.ts +40 -0
- package/src/compat/actions/endpoint.ts +602 -0
- package/src/compat/actions/flight.ts +52 -0
- package/src/compat/actions/form-state.ts +73 -0
- package/src/compat/actions/hoist.ts +485 -0
- package/src/compat/actions/ids.ts +39 -0
- package/src/compat/actions/index.ts +41 -0
- package/src/compat/actions/instances.ts +144 -0
- package/src/compat/actions/origin.ts +109 -0
- package/src/compat/actions/protocol.ts +125 -0
- package/src/compat/actions/registry.ts +74 -0
- package/src/compat/actions/rewrite.ts +282 -0
- package/src/compat/actions/serve.ts +414 -0
- package/src/compat/actions/server-tag.ts +21 -0
- package/src/compat/actions/unrecognized-error.ts +30 -0
- package/src/compat/adapter/build-complete.ts +257 -0
- package/src/compat/bundler/bun-externals.ts +53 -0
- package/src/compat/bundler/cjs-exports.ts +542 -0
- package/src/compat/bundler/config.ts +363 -0
- package/src/compat/bundler/externals.ts +34 -0
- package/src/compat/bundler/import-meta-url.ts +60 -0
- package/src/compat/bundler/modularize-imports.ts +119 -0
- package/src/compat/bundler/new-url-asset.ts +87 -0
- package/src/compat/bundler/optimize-package-imports.ts +273 -0
- package/src/compat/bundler/polyfill.ts +88 -0
- package/src/compat/bundler/react-compiler.ts +61 -0
- package/src/compat/bundler/react-profiler.tsx +25 -0
- package/src/compat/bundler/relay-transform.ts +116 -0
- package/src/compat/bundler/require-context.ts +281 -0
- package/src/compat/bundler/resolve-extensions.ts +75 -0
- package/src/compat/bundler/source-cache.ts +61 -0
- package/src/compat/bundler/static-imports.ts +25 -0
- package/src/compat/bundler/symlink-imports.ts +119 -0
- package/src/compat/bundler/tsconfig-paths.ts +50 -0
- package/src/compat/bundler/wasm.ts +153 -0
- package/src/compat/bundler/webpack-loaders.ts +685 -0
- package/src/compat/bundler/worker.ts +278 -0
- package/src/compat/cache/build-flags.ts +81 -0
- package/src/compat/cache/build-prerender-errors.ts +163 -0
- package/src/compat/cache/custom-handler.ts +159 -0
- package/src/compat/cache/fetch-patch.ts +745 -0
- package/src/compat/cache/handler.ts +100 -0
- package/src/compat/cache/modern-handler.ts +275 -0
- package/src/compat/cache/resume-data-cache.ts +143 -0
- package/src/compat/cache/revalidate.ts +759 -0
- package/src/compat/cache/runtime-error.ts +124 -0
- package/src/compat/cache/use-cache-transform.ts +961 -0
- package/src/compat/cache/use-cache.ts +1695 -0
- package/src/compat/cache-control.ts +269 -0
- package/src/compat/client/base-path.ts +64 -0
- package/src/compat/client/css-order.ts +36 -0
- package/src/compat/client/errors/control-flow.ts +92 -0
- package/src/compat/client/errors/error-boundary.ts +222 -0
- package/src/compat/client/errors/global-error.ts +238 -0
- package/src/compat/client/errors/install.ts +217 -0
- package/src/compat/client/errors/lazy.ts +53 -0
- package/src/compat/client/errors/primitive-throw.ts +126 -0
- package/src/compat/client/errors/soft-refresh.ts +14 -0
- package/src/compat/client/link-status.ts +86 -0
- package/src/compat/client/nav-compat-runtime.ts +57 -0
- package/src/compat/client/nav-compat.ts +42 -0
- package/src/compat/client/navigation-scroll.ts +154 -0
- package/src/compat/client/optimistic-routing.ts +206 -0
- package/src/compat/client/prefetch-cache.ts +111 -0
- package/src/compat/client/route-announcer.ts +72 -0
- package/src/compat/client/segment-cache-policy.ts +159 -0
- package/src/compat/client/segment-cache.ts +1077 -0
- package/src/compat/client/segment-prefetch.ts +375 -0
- package/src/compat/client/trailing-slash.ts +24 -0
- package/src/compat/css/chunking.ts +254 -0
- package/src/compat/css/inline-css.ts +73 -0
- package/src/compat/css/lightningcss.ts +90 -0
- package/src/compat/css/modules.ts +373 -0
- package/src/compat/css/nonce.ts +30 -0
- package/src/compat/css/sass-plugin.ts +65 -0
- package/src/compat/css/sass.ts +392 -0
- package/src/compat/css/styled-jsx-runtime.ts +80 -0
- package/src/compat/css/styled-jsx.ts +49 -0
- package/src/compat/edge-runtime.ts +71 -0
- package/src/compat/export/client.ts +112 -0
- package/src/compat/export/index.ts +272 -0
- package/src/compat/export/standalone.ts +207 -0
- package/src/compat/image-optimizer/cache.ts +119 -0
- package/src/compat/image-optimizer/detect.ts +143 -0
- package/src/compat/image-optimizer/index.ts +601 -0
- package/src/compat/image-optimizer/source.ts +243 -0
- package/src/compat/index.ts +458 -0
- package/src/compat/lifecycle/after-scope.ts +86 -0
- package/src/compat/lifecycle/after.ts +173 -0
- package/src/compat/lifecycle/error-funnel.ts +306 -0
- package/src/compat/lifecycle/error-serialize.ts +87 -0
- package/src/compat/lifecycle/error-ui.ts +167 -0
- package/src/compat/lifecycle/instrumentation-client.ts +138 -0
- package/src/compat/lifecycle/instrumentation.ts +277 -0
- package/src/compat/lifecycle/node-console.ts +19 -0
- package/src/compat/lifecycle/testmode.ts +263 -0
- package/src/compat/mdx/compile.ts +219 -0
- package/src/compat/mdx/next-mdx-stub.ts +46 -0
- package/src/compat/mdx/plugin.ts +37 -0
- package/src/compat/metadata-route-artifacts.ts +458 -0
- package/src/compat/metadata.ts +295 -0
- package/src/compat/middleware/manifest.ts +210 -0
- package/src/compat/misc/action-return.ts +173 -0
- package/src/compat/next/cache.ts +211 -0
- package/src/compat/next/canonical-url.ts +35 -0
- package/src/compat/next/client-cache.ts +57 -0
- package/src/compat/next/client-navigation.ts +313 -0
- package/src/compat/next/client-only.ts +3 -0
- package/src/compat/next/client-script.tsx +215 -0
- package/src/compat/next/client-server.ts +39 -0
- package/src/compat/next/config-loader.ts +569 -0
- package/src/compat/next/config.ts +29 -0
- package/src/compat/next/constants.cjs +6 -0
- package/src/compat/next/constants.ts +6 -0
- package/src/compat/next/custom-server.ts +236 -0
- package/src/compat/next/dist/client/components/app-router-headers.ts +32 -0
- package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +38 -0
- package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -0
- package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -0
- package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -0
- package/src/compat/next/dynamic.tsx +46 -0
- package/src/compat/next/error.tsx +148 -0
- package/src/compat/next/font/cache.ts +171 -0
- package/src/compat/next/font/google.ts +2 -0
- package/src/compat/next/font/index.ts +8 -0
- package/src/compat/next/font/local.ts +5 -0
- package/src/compat/next/font/runtime-client.ts +71 -0
- package/src/compat/next/font/runtime.ts +974 -0
- package/src/compat/next/font/shared.ts +281 -0
- package/src/compat/next/form.tsx +156 -0
- package/src/compat/next/head.tsx +10 -0
- package/src/compat/next/headers.ts +247 -0
- package/src/compat/next/image/config.ts +196 -0
- package/src/compat/next/image/optimizer.ts +96 -0
- package/src/compat/next/image/patterns.ts +103 -0
- package/src/compat/next/image/shared.ts +141 -0
- package/src/compat/next/image/static-metadata.ts +283 -0
- package/src/compat/next/image/validate.ts +269 -0
- package/src/compat/next/image-client.tsx +215 -0
- package/src/compat/next/image-props.ts +575 -0
- package/src/compat/next/image-usage.ts +102 -0
- package/src/compat/next/image.tsx +56 -0
- package/src/compat/next/index.ts +1 -0
- package/src/compat/next/legacy-image.tsx +97 -0
- package/src/compat/next/link-usage.ts +29 -0
- package/src/compat/next/link-validation-transform.ts +200 -0
- package/src/compat/next/link.tsx +466 -0
- package/src/compat/next/navigation.cjs +21 -0
- package/src/compat/next/navigation.ts +188 -0
- package/src/compat/next/offline.ts +51 -0
- package/src/compat/next/og.ts +324 -0
- package/src/compat/next/optimistic-route-state.ts +188 -0
- package/src/compat/next/preferred-region.ts +39 -0
- package/src/compat/next/redirects.ts +131 -0
- package/src/compat/next/resource-hints.ts +136 -0
- package/src/compat/next/rewrites.ts +350 -0
- package/src/compat/next/root-params.ts +142 -0
- package/src/compat/next/router.cjs +49 -0
- package/src/compat/next/router.ts +143 -0
- package/src/compat/next/script.tsx +355 -0
- package/src/compat/next/server-only.ts +3 -0
- package/src/compat/next/server.ts +28 -0
- package/src/compat/next/svgr.ts +58 -0
- package/src/compat/next/telemetry.ts +77 -0
- package/src/compat/next/user-agent.ts +100 -0
- package/src/compat/next/web-vitals.ts +56 -0
- package/src/compat/otel/api.ts +95 -0
- package/src/compat/otel/client-trace-metadata.ts +71 -0
- package/src/compat/otel/fetch-span.ts +77 -0
- package/src/compat/otel/tracer.ts +944 -0
- package/src/compat/pages/client-plugin.ts +108 -0
- package/src/compat/pages/index.ts +527 -0
- package/src/compat/pages/router-state.ts +94 -0
- package/src/compat/ppr/io.ts +38 -0
- package/src/compat/ppr/missing-root-params.ts +105 -0
- package/src/compat/ppr/root-params-scan.ts +164 -0
- package/src/compat/ppr/root-params-transform.ts +75 -0
- package/src/compat/ppr/root-params.ts +129 -0
- package/src/compat/ppr/segment-config-incompat.ts +34 -0
- package/src/compat/protocol.ts +202 -0
- package/src/compat/react/client.ts +59 -0
- package/src/compat/react/compiler-runtime.ts +60 -0
- package/src/compat/react/dom-client.ts +115 -0
- package/src/compat/react/dom-react-server.ts +20 -0
- package/src/compat/react/dom-server.ts +40 -0
- package/src/compat/react/dom.ts +154 -0
- package/src/compat/react/preact.ts +522 -0
- package/src/compat/react/react-server.ts +84 -0
- package/src/compat/react/router-shim.ts +26 -0
- package/src/compat/react/server-component-use.ts +48 -0
- package/src/compat/react/server-inserted-html.ts +87 -0
- package/src/compat/react/server.ts +156 -0
- package/src/compat/react/view-transition.ts +60 -0
- package/src/compat/register/actions.ts +875 -0
- package/src/compat/register/boot.ts +141 -0
- package/src/compat/register/build-tier.ts +11 -0
- package/src/compat/register/build.ts +182 -0
- package/src/compat/register/bundler.ts +587 -0
- package/src/compat/register/cache.ts +85 -0
- package/src/compat/register/client-errors.ts +18 -0
- package/src/compat/register/config.ts +17 -0
- package/src/compat/register/css-extras.ts +120 -0
- package/src/compat/register/edge-runtime.ts +6 -0
- package/src/compat/register/errors.ts +46 -0
- package/src/compat/register/export.ts +23 -0
- package/src/compat/register/font.ts +36 -0
- package/src/compat/register/hooks.ts +34 -0
- package/src/compat/register/image.ts +133 -0
- package/src/compat/register/index.ts +111 -0
- package/src/compat/register/instrumentation-client.ts +35 -0
- package/src/compat/register/lifecycle.ts +86 -0
- package/src/compat/register/mdx.ts +48 -0
- package/src/compat/register/middleware.ts +36 -0
- package/src/compat/register/misc.ts +44 -0
- package/src/compat/register/otel.ts +288 -0
- package/src/compat/register/pages-api.ts +473 -0
- package/src/compat/register/ppr.ts +56 -0
- package/src/compat/register/protocol.ts +57 -0
- package/src/compat/register/proxy.ts +127 -0
- package/src/compat/register/render.ts +268 -0
- package/src/compat/register/routing.ts +410 -0
- package/src/compat/register/segment.ts +1903 -0
- package/src/compat/register/static-image.ts +21 -0
- package/src/compat/register/typed-routes.ts +35 -0
- package/src/compat/register/usecache.ts +131 -0
- package/src/compat/register/validation.ts +56 -0
- package/src/compat/segment/loading-boundary.ts +113 -0
- package/src/compat/segment/page-slot.ts +200 -0
- package/src/compat/segment/tree.ts +481 -0
- package/src/compat/segment/vary-key.ts +102 -0
- package/src/compat/segment/vary-params.ts +551 -0
- package/src/compat/static-params.ts +33 -0
- package/src/compat/tsconfig-defaults.ts +301 -0
- package/src/compat/typecheck/index.ts +1481 -0
- package/src/compat/typecheck/worker.ts +26 -0
- package/src/compat/typed-routes/index.ts +92 -0
- package/src/compat/typed-routes/manifest.ts +356 -0
- package/src/compat/typed-routes/typegen.ts +566 -0
- package/src/compat/validation/errors.ts +159 -0
- package/src/compat/validation/index.ts +1770 -0
- package/src/compat/validation/prerender-diagnostics.ts +1508 -0
- package/src/compat-bootstrap.ts +67 -0
- package/src/config.ts +218 -0
- package/src/css/build.ts +697 -0
- package/src/css/index.ts +2 -0
- package/src/css/postcss.ts +236 -0
- package/src/css/worker.ts +34 -0
- package/src/dev/client-actions.ts +35 -0
- package/src/dev/client-chunk-store.ts +92 -0
- package/src/dev/client-key-cache.ts +178 -0
- package/src/dev/global-css-cache.ts +212 -0
- package/src/dev/imports.ts +2430 -0
- package/src/dev/module-cache.ts +721 -0
- package/src/dev/module-generations.ts +38 -0
- package/src/dev/module-transform.ts +188 -0
- package/src/dev/node-module-bundle-cache.ts +63 -0
- package/src/dev/restart-cache.ts +10 -0
- package/src/dev/route-bundle-key-cache.ts +154 -0
- package/src/dev/route-facts-cache.ts +223 -0
- package/src/dev/server.ts +1710 -0
- package/src/dynamic/source.ts +307 -0
- package/src/dynamic/tree-shake.ts +262 -0
- package/src/env.ts +92 -0
- package/src/extensions.ts +1898 -0
- package/src/index.ts +34 -0
- package/src/internal.ts +43 -0
- package/src/islands/boundary-error.ts +8 -0
- package/src/islands/static-children.ts +37 -0
- package/src/islands/static-slots.ts +106 -0
- package/src/ppr-postpone.ts +24 -0
- package/src/ppr.ts +784 -0
- package/src/proxy.ts +752 -0
- package/src/render/hooks.ts +384 -0
- package/src/render/index.ts +1 -0
- package/src/render/island-context.ts +47 -0
- package/src/render/metadata.ts +857 -0
- package/src/render/renderer.ts +7391 -0
- package/src/render/resource-hints.ts +44 -0
- package/src/render/slots.tsx +679 -0
- package/src/request/context.ts +396 -0
- package/src/resolve/engine.ts +219 -0
- package/src/resolve/imports.ts +1104 -0
- package/src/resolve/scan-facts.ts +474 -0
- package/src/resolve/source-text.ts +86 -0
- package/src/routing/forwarded.ts +41 -0
- package/src/routing/handler.ts +271 -0
- package/src/routing/href.ts +203 -0
- package/src/routing/metadata.ts +1018 -0
- package/src/routing/request-runtime.ts +43 -0
- package/src/routing/routes.ts +2560 -0
- package/src/routing/slots.ts +432 -0
- package/src/runtime/server.ts +3453 -0
- package/src/runtime/vendor.ts +1160 -0
- package/src/style-modules.d.ts +9 -0
- package/src/typegen.ts +151 -0
- package/src/types.ts +725 -0
- package/src/utils/ansi.ts +9 -0
- package/src/utils/content-type.ts +31 -0
- package/src/utils/decode.ts +7 -0
- package/src/utils/dev-profile.ts +31 -0
- package/src/utils/error-log.ts +29 -0
- package/src/utils/fs-cache.ts +31 -0
- package/src/utils/fs.ts +119 -0
- package/src/utils/html.ts +46 -0
- package/src/utils/serialize.ts +378 -0
- package/src/utils/source.ts +35 -0
- package/src/utils/verbose.ts +39 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,961 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source rewrite for the 'use cache' directive (Next compat). Regex/heuristic based like the
|
|
3
|
+
* 'use server' scanning in compat/actions/detect.ts - not a compiler. Two shapes are handled:
|
|
4
|
+
*
|
|
5
|
+
* 1. Function-level: a function whose body opens with the directive. The directive is stripped and the
|
|
6
|
+
* function's binding is reassigned to `pnextUseCache(id, fn)` at the end of the module (const
|
|
7
|
+
* declarations are rewritten to let).
|
|
8
|
+
* 2. Module-level: a file whose first statement is the directive. Every exported function declaration
|
|
9
|
+
* gets wrapped the same way.
|
|
10
|
+
*
|
|
11
|
+
* Limitations: anonymous default exports, object methods, and function expressions passed inline as
|
|
12
|
+
* arguments are not wrapped.
|
|
13
|
+
*/
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
|
|
16
|
+
const useCacheRuntimePath = path.join(import.meta.dirname, 'use-cache.ts');
|
|
17
|
+
|
|
18
|
+
const quickTest = /(['"])use cache(?:\s*:\s*[\w-]+)?\1/;
|
|
19
|
+
const directiveText = /(['"])use cache(?:\s*:\s*([\w-]+))?\1\s*;?/;
|
|
20
|
+
const rootParamsImportLine =
|
|
21
|
+
/^[ \t]*import\s+\{[\s\S]*?\}\s+from\s+['"]next\/root-params['"]\s*;?[ \t]*(?:\r?\n)?/gm;
|
|
22
|
+
|
|
23
|
+
const boundHelper = '__pnextUseCacheBound';
|
|
24
|
+
|
|
25
|
+
export function rewriteUseCacheSource(source: string, file: string, root?: string): string {
|
|
26
|
+
// `includes` first: over a real module corpus the quoted-directive regex is
|
|
27
|
+
// ~24x the cost of the token scan, and virtually every module fails the token.
|
|
28
|
+
if (!source.includes('use cache') || !quickTest.test(source)) return source;
|
|
29
|
+
|
|
30
|
+
const idBase = root ? path.relative(root, file) : file;
|
|
31
|
+
const wrapped = new Set<string>();
|
|
32
|
+
const privateWrapped = new Set<string>();
|
|
33
|
+
let out = source;
|
|
34
|
+
let usedBoundHelper = false;
|
|
35
|
+
|
|
36
|
+
out = rewriteModuleLevelDirective(out, wrapped);
|
|
37
|
+
// Nested cached closures capturing enclosing bindings (bound args) must be wrapped in place - a
|
|
38
|
+
// module-level reassignment cannot reach a function-scoped binding, and the captured values
|
|
39
|
+
// participate in the cache key. Runs before the top-level pass so it only claims genuinely-nested
|
|
40
|
+
// directives. Returned arrow closures capture their enclosing scope's free variables as bound args,
|
|
41
|
+
// including any non-serializable function, which is a build error - wrap them first so the directive
|
|
42
|
+
// is claimed here before the nested-named-closure pass, whose brace-balance heuristic would otherwise
|
|
43
|
+
// mis-attribute the arrow's directive to a sibling function declaration.
|
|
44
|
+
const arrows = rewriteReturnedArrowClosures(out, idBase);
|
|
45
|
+
out = arrows.source;
|
|
46
|
+
usedBoundHelper = arrows.wrapped;
|
|
47
|
+
const nested = rewriteNestedBoundClosures(out, idBase);
|
|
48
|
+
out = nested.source;
|
|
49
|
+
usedBoundHelper = usedBoundHelper || nested.wrapped;
|
|
50
|
+
// Function expressions in value position (JSX attributes / call arguments):
|
|
51
|
+
// `<Form foo={async function () { 'use cache'; … }} />`.
|
|
52
|
+
const expressions = rewriteExpressionClosures(out, idBase);
|
|
53
|
+
out = expressions.source;
|
|
54
|
+
usedBoundHelper = usedBoundHelper || expressions.wrapped;
|
|
55
|
+
const objectMethods = rewriteObjectMethods(out, idBase);
|
|
56
|
+
out = objectMethods.source;
|
|
57
|
+
usedBoundHelper = usedBoundHelper || objectMethods.wrapped;
|
|
58
|
+
const staticMethods = rewriteStaticClassMethods(out, idBase);
|
|
59
|
+
out = staticMethods.source;
|
|
60
|
+
const usedDirectHelper = staticMethods.wrapped;
|
|
61
|
+
out = rewriteFunctionLevelDirectives(out, wrapped, privateWrapped);
|
|
62
|
+
|
|
63
|
+
if (wrapped.size === 0 && !usedBoundHelper && !usedDirectHelper) return out;
|
|
64
|
+
|
|
65
|
+
const imports: string[] = [];
|
|
66
|
+
if (wrapped.size > 0 || usedDirectHelper) {
|
|
67
|
+
imports.push(`import { pnextUseCache as __pnextUseCache } from ${JSON.stringify(useCacheRuntimePath)};`);
|
|
68
|
+
}
|
|
69
|
+
if (privateWrapped.size > 0) {
|
|
70
|
+
imports.push(
|
|
71
|
+
`import { pnextUseCachePrivate as __pnextUseCachePrivate } from ${JSON.stringify(useCacheRuntimePath)};`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (usedBoundHelper) {
|
|
75
|
+
imports.push(
|
|
76
|
+
`import { pnextUseCacheBound as ${boundHelper} } from ${JSON.stringify(useCacheRuntimePath)};`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
// `export default NAME;` snapshots the binding's value when the statement
|
|
80
|
+
// evaluates (a default-exported identifier is not a live binding, unlike
|
|
81
|
+
// `export default function NAME`), so the module-end reassignment would be
|
|
82
|
+
// invisible to importers. Move the statement after the reassignments.
|
|
83
|
+
let defaultExport = '';
|
|
84
|
+
for (const name of wrapped) {
|
|
85
|
+
const statement = new RegExp(
|
|
86
|
+
`(^|[\\n;])[ \\t]*export\\s+default\\s+${escapeIdent(name)}(?![\\w$])\\s*;?`,
|
|
87
|
+
).exec(out);
|
|
88
|
+
if (!statement) continue;
|
|
89
|
+
out =
|
|
90
|
+
out.slice(0, statement.index) +
|
|
91
|
+
(statement[1] ?? '') +
|
|
92
|
+
out.slice(statement.index + statement[0].length);
|
|
93
|
+
defaultExport = `\nexport default ${name};`;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const hoisted = hoistRootParamsImports(out);
|
|
98
|
+
const assignments = [...wrapped]
|
|
99
|
+
.map(name =>
|
|
100
|
+
privateWrapped.has(name)
|
|
101
|
+
? `${name} = __pnextUseCachePrivate(${JSON.stringify(`${idBase}#${name}`)}, ${name});`
|
|
102
|
+
: `${name} = __pnextUseCache(${JSON.stringify(`${idBase}#${name}`)}, ${name});`,
|
|
103
|
+
)
|
|
104
|
+
.join('\n');
|
|
105
|
+
const suffix = assignments ? `\n;${assignments}${defaultExport}\n` : '\n';
|
|
106
|
+
return `${hoisted.imports}${imports.join('\n')}\n${hoisted.source}${suffix}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function hoistRootParamsImports(source: string): { imports: string; source: string } {
|
|
110
|
+
if (!source.includes('next/root-params')) return { imports: '', source };
|
|
111
|
+
const imports: string[] = [];
|
|
112
|
+
const body = source.replace(rootParamsImportLine, statement => {
|
|
113
|
+
imports.push(statement.trimEnd());
|
|
114
|
+
return '';
|
|
115
|
+
});
|
|
116
|
+
if (imports.length === 0) return { imports: '', source };
|
|
117
|
+
return {
|
|
118
|
+
imports: `${imports.join('\n')}\n`,
|
|
119
|
+
source: body.replace(/^\s*\n/, ''),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Wrap nested cached closures (a `'use cache'` function declared inside another function) in place with
|
|
125
|
+
* `pnextUseCacheBound`, threading the enclosing function's parameters that the closure references as
|
|
126
|
+
* bound-arg key inputs. Handles the load-bearing shape:
|
|
127
|
+
*
|
|
128
|
+
* async function outer(arg) {
|
|
129
|
+
* async function inner() { 'use cache'; return { arg } }
|
|
130
|
+
* return inner()
|
|
131
|
+
* }
|
|
132
|
+
*
|
|
133
|
+
* Captures are limited to the nearest enclosing function's parameter names that appear as whole-word
|
|
134
|
+
* identifiers in the closure body - enough for the fixtures without a full scope analysis, and
|
|
135
|
+
* conservative (never captures globals).
|
|
136
|
+
*/
|
|
137
|
+
function rewriteNestedBoundClosures(
|
|
138
|
+
source: string,
|
|
139
|
+
idBase: string,
|
|
140
|
+
): { source: string; wrapped: boolean } {
|
|
141
|
+
let out = source;
|
|
142
|
+
let wrapped = false;
|
|
143
|
+
let searchFrom = 0;
|
|
144
|
+
for (;;) {
|
|
145
|
+
const tail = out.slice(searchFrom);
|
|
146
|
+
const directive = directiveText.exec(tail);
|
|
147
|
+
if (!directive) break;
|
|
148
|
+
const index = searchFrom + directive.index;
|
|
149
|
+
const prefix = out.slice(0, index);
|
|
150
|
+
if (!/\{\s*$/.test(prefix)) {
|
|
151
|
+
searchFrom = index + directive[0].length;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const closure = findNestedClosure(out, index);
|
|
155
|
+
if (!closure) {
|
|
156
|
+
searchFrom = index + directive[0].length;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const id = `${idBase}#${closure.enclosingName}.${closure.name}`;
|
|
160
|
+
const captureList = closure.captures.length ? closure.captures.join(', ') : '';
|
|
161
|
+
// Replace `[async ]function NAME(params) {` with a wrapped const binding, and
|
|
162
|
+
// strip the directive from the body.
|
|
163
|
+
const wrappedHead =
|
|
164
|
+
`const ${closure.name} = ${boundHelper}(${JSON.stringify(id)}, ${closure.asyncKeyword}function ${closure.name}(${closure.params}) {`;
|
|
165
|
+
const bodyAfterDirective = out.slice(index + directive[0].length, closure.bodyEnd);
|
|
166
|
+
const rest = out.slice(closure.bodyEnd);
|
|
167
|
+
out =
|
|
168
|
+
out.slice(0, closure.declStart) +
|
|
169
|
+
wrappedHead +
|
|
170
|
+
bodyAfterDirective +
|
|
171
|
+
`}, () => [${captureList}])` +
|
|
172
|
+
rest.replace(/^\}/, '');
|
|
173
|
+
wrapped = true;
|
|
174
|
+
searchFrom = closure.declStart + wrappedHead.length;
|
|
175
|
+
}
|
|
176
|
+
return { source: out, wrapped };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Wrap `use cache` closures returned as arrow functions from an enclosing function - the shape the
|
|
181
|
+
* close-over-function fixture uses:
|
|
182
|
+
*
|
|
183
|
+
* function createCachedFn(start) {
|
|
184
|
+
* function fn() { return start }
|
|
185
|
+
* return async () => { 'use cache'; return Math.random() + fn() }
|
|
186
|
+
* }
|
|
187
|
+
*
|
|
188
|
+
* The capture list is the enclosing function's params and local declarations that the closure
|
|
189
|
+
* references, so a captured function reaches the runtime's close-over-function guard. Scoped to
|
|
190
|
+
* return-position arrows to keep the regex heuristic narrow; arrows elsewhere stay inert.
|
|
191
|
+
*/
|
|
192
|
+
function rewriteReturnedArrowClosures(
|
|
193
|
+
source: string,
|
|
194
|
+
idBase: string,
|
|
195
|
+
): { source: string; wrapped: boolean } {
|
|
196
|
+
let out = source;
|
|
197
|
+
let wrapped = false;
|
|
198
|
+
let searchFrom = 0;
|
|
199
|
+
let counter = 0;
|
|
200
|
+
for (;;) {
|
|
201
|
+
const tail = out.slice(searchFrom);
|
|
202
|
+
const directive = directiveText.exec(tail);
|
|
203
|
+
if (!directive) break;
|
|
204
|
+
const index = searchFrom + directive.index;
|
|
205
|
+
const prefix = out.slice(0, index);
|
|
206
|
+
if (!/\{\s*$/.test(prefix)) {
|
|
207
|
+
searchFrom = index + directive[0].length;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const closure = findReturnedArrowClosure(out, index);
|
|
211
|
+
if (!closure) {
|
|
212
|
+
searchFrom = index + directive[0].length;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const id = `${idBase}#${closure.enclosingName}.arrow${counter === 0 ? '' : counter}`;
|
|
216
|
+
counter += 1;
|
|
217
|
+
const captureList = closure.captures.join(', ');
|
|
218
|
+
const wrappedHead = `${boundHelper}(${JSON.stringify(id)}, ${closure.head}`;
|
|
219
|
+
const bodyAfterDirective = out.slice(index + directive[0].length, closure.bodyEnd);
|
|
220
|
+
const rest = out.slice(closure.bodyEnd);
|
|
221
|
+
out =
|
|
222
|
+
out.slice(0, closure.declStart) +
|
|
223
|
+
wrappedHead +
|
|
224
|
+
bodyAfterDirective +
|
|
225
|
+
`}, () => [${captureList}])` +
|
|
226
|
+
rest.replace(/^\}/, '');
|
|
227
|
+
wrapped = true;
|
|
228
|
+
searchFrom = closure.declStart + wrappedHead.length;
|
|
229
|
+
}
|
|
230
|
+
return { source: out, wrapped };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Wrap `use cache` function expressions in VALUE position - JSX attributes, call arguments and
|
|
235
|
+
* object-property values. Captures are the nearest enclosing named function's params/locals referenced
|
|
236
|
+
* in the body (same policy as the returned-arrow pass). Statement declarations and top-level bindings
|
|
237
|
+
* are NOT claimed here - the value-position context requires a preceding `={`, `(`, `,` or `:` - so the
|
|
238
|
+
* established passes keep their shapes and id scheme. Named-handler variants are left inert.
|
|
239
|
+
*/
|
|
240
|
+
function rewriteExpressionClosures(
|
|
241
|
+
source: string,
|
|
242
|
+
idBase: string,
|
|
243
|
+
): { source: string; wrapped: boolean } {
|
|
244
|
+
let out = source;
|
|
245
|
+
let wrapped = false;
|
|
246
|
+
let searchFrom = 0;
|
|
247
|
+
let counter = 0;
|
|
248
|
+
for (;;) {
|
|
249
|
+
const tail = out.slice(searchFrom);
|
|
250
|
+
const directive = directiveText.exec(tail);
|
|
251
|
+
if (!directive) break;
|
|
252
|
+
const index = searchFrom + directive.index;
|
|
253
|
+
if (directive[2] !== undefined || !/\{\s*$/.test(out.slice(0, index))) {
|
|
254
|
+
searchFrom = index + directive[0].length;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const closure = findExpressionClosure(out, index);
|
|
258
|
+
if (!closure) {
|
|
259
|
+
searchFrom = index + directive[0].length;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const id = `${idBase}#${closure.enclosingName}.expr${counter === 0 ? '' : counter}`;
|
|
263
|
+
counter += 1;
|
|
264
|
+
const wrappedHead = `${boundHelper}(${JSON.stringify(id)}, ${closure.head}`;
|
|
265
|
+
const bodyAfterDirective = out.slice(index + directive[0].length, closure.bodyEnd);
|
|
266
|
+
const rest = out.slice(closure.bodyEnd);
|
|
267
|
+
out =
|
|
268
|
+
out.slice(0, closure.declStart) +
|
|
269
|
+
wrappedHead +
|
|
270
|
+
bodyAfterDirective +
|
|
271
|
+
`}, () => [${closure.captures.join(', ')}])` +
|
|
272
|
+
rest.replace(/^\}/, '');
|
|
273
|
+
wrapped = true;
|
|
274
|
+
searchFrom = closure.declStart + wrappedHead.length;
|
|
275
|
+
}
|
|
276
|
+
return { source: out, wrapped };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function findExpressionClosure(
|
|
280
|
+
source: string,
|
|
281
|
+
directiveIndex: number,
|
|
282
|
+
): ReturnedArrowClosure | null {
|
|
283
|
+
const prefix = source.slice(0, directiveIndex);
|
|
284
|
+
// Value-position contexts: a JSX attribute (`={`), a call argument (`(` or
|
|
285
|
+
// `,`), or an object-property value (`:`), followed by a function expression
|
|
286
|
+
// or an arrow whose body opens with the directive.
|
|
287
|
+
const exprRe =
|
|
288
|
+
/(=\s*\{|[(,:])\s*((?:async\s+)?(?:function(?:\s+[A-Za-z_$][\w$]*)?\s*\(([^)]*)\)|(?:\(([^)]*)\)|[A-Za-z_$][\w$]*)\s*=>)\s*\{)/g;
|
|
289
|
+
let match: RegExpExecArray | null;
|
|
290
|
+
let best: { headStart: number; head: string; params: string; bodyOpen: number } | null = null;
|
|
291
|
+
while ((match = exprRe.exec(prefix)) !== null) {
|
|
292
|
+
const head = match[2]!;
|
|
293
|
+
const headStart = match.index + match[0].length - head.length;
|
|
294
|
+
const bodyOpen = match.index + match[0].length;
|
|
295
|
+
const between = source.slice(bodyOpen, directiveIndex);
|
|
296
|
+
// The directive must be the first statement of THIS body: zero net braces
|
|
297
|
+
// between, and the body must never close in between (a dip below zero means
|
|
298
|
+
// this candidate's body ended before the directive).
|
|
299
|
+
let balance = 0;
|
|
300
|
+
let closed = false;
|
|
301
|
+
for (const ch of between) {
|
|
302
|
+
if (ch === '{') balance += 1;
|
|
303
|
+
else if (ch === '}') {
|
|
304
|
+
balance -= 1;
|
|
305
|
+
if (balance < 0) {
|
|
306
|
+
closed = true;
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (!closed && balance === 0) {
|
|
312
|
+
best = { headStart, head, params: match[3] ?? match[4] ?? '', bodyOpen };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (!best) return null;
|
|
316
|
+
|
|
317
|
+
const bodyEnd = matchClosingBrace(source, best.bodyOpen);
|
|
318
|
+
if (bodyEnd < directiveIndex) return null;
|
|
319
|
+
const body = source.slice(best.bodyOpen, bodyEnd);
|
|
320
|
+
|
|
321
|
+
const enclosing = findEnclosingFunctionFull(source, best.headStart);
|
|
322
|
+
const ownParams = new Set(paramNames(best.params));
|
|
323
|
+
const captures = enclosing
|
|
324
|
+
? enclosingCaptureCandidates(source, enclosing).filter(
|
|
325
|
+
name => !ownParams.has(name) && new RegExp(`\\b${escapeIdent(name)}\\b`).test(body),
|
|
326
|
+
)
|
|
327
|
+
: [];
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
head: best.head,
|
|
331
|
+
declStart: best.headStart,
|
|
332
|
+
bodyEnd,
|
|
333
|
+
enclosingName: enclosing?.name ?? 'module',
|
|
334
|
+
captures,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
interface ReturnedArrowClosure {
|
|
339
|
+
head: string;
|
|
340
|
+
declStart: number;
|
|
341
|
+
bodyEnd: number;
|
|
342
|
+
enclosingName: string;
|
|
343
|
+
captures: string[];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function findReturnedArrowClosure(
|
|
347
|
+
source: string,
|
|
348
|
+
directiveIndex: number,
|
|
349
|
+
): ReturnedArrowClosure | null {
|
|
350
|
+
const prefix = source.slice(0, directiveIndex);
|
|
351
|
+
// The parameter list must stay on one line and carry no `<`: otherwise
|
|
352
|
+
// `return (\n <Form getDate={async () => {` reads as one giant "arrow" whose
|
|
353
|
+
// params run from the returned parenthesis to the first `)` inside the JSX,
|
|
354
|
+
// and the wrap lands on the return expression instead of the attribute's
|
|
355
|
+
// closure (use-cache-with-server-function-props). Value-position closures like
|
|
356
|
+
// that are claimed by rewriteExpressionClosures instead.
|
|
357
|
+
const arrowRe = /\breturn\s+(async\s+)?(\([^)<\n]*\)|[A-Za-z_$][\w$]*)\s*=>\s*\{/g;
|
|
358
|
+
let match: RegExpExecArray | null;
|
|
359
|
+
let best: { headStart: number; head: string; params: string; bodyOpen: number } | null = null;
|
|
360
|
+
while ((match = arrowRe.exec(prefix)) !== null) {
|
|
361
|
+
const retPrefix = /^return\s+/.exec(match[0])?.[0] ?? 'return ';
|
|
362
|
+
const headStart = match.index + retPrefix.length;
|
|
363
|
+
const bodyOpen = match.index + match[0].length;
|
|
364
|
+
const between = source.slice(bodyOpen, directiveIndex);
|
|
365
|
+
let balance = 0;
|
|
366
|
+
for (const ch of between) {
|
|
367
|
+
if (ch === '{') balance += 1;
|
|
368
|
+
else if (ch === '}') balance -= 1;
|
|
369
|
+
}
|
|
370
|
+
if (balance === 0) {
|
|
371
|
+
const paramSpec = match[2]!;
|
|
372
|
+
const params = paramSpec.startsWith('(') ? paramSpec.slice(1, -1) : paramSpec;
|
|
373
|
+
best = { headStart, head: match[0].slice(retPrefix.length), params, bodyOpen };
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (!best) return null;
|
|
377
|
+
|
|
378
|
+
const enclosing = findEnclosingFunctionFull(source, best.headStart);
|
|
379
|
+
if (!enclosing) return null;
|
|
380
|
+
|
|
381
|
+
const bodyEnd = matchClosingBrace(source, best.bodyOpen);
|
|
382
|
+
if (bodyEnd < 0) return null;
|
|
383
|
+
const body = source.slice(best.bodyOpen, bodyEnd);
|
|
384
|
+
|
|
385
|
+
const arrowParams = new Set(paramNames(best.params));
|
|
386
|
+
const candidates = enclosingCaptureCandidates(source, enclosing);
|
|
387
|
+
const captures = candidates.filter(
|
|
388
|
+
name => !arrowParams.has(name) && new RegExp(`\\b${escapeIdent(name)}\\b`).test(body),
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
head: best.head,
|
|
393
|
+
declStart: best.headStart,
|
|
394
|
+
bodyEnd,
|
|
395
|
+
enclosingName: enclosing.name,
|
|
396
|
+
captures,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Nearest enclosing named function wrapping `pos`, with its body span. */
|
|
401
|
+
function findEnclosingFunctionFull(
|
|
402
|
+
source: string,
|
|
403
|
+
pos: number,
|
|
404
|
+
): { name: string; params: string[]; bodyStart: number; bodyEnd: number } | null {
|
|
405
|
+
const before = source.slice(0, pos);
|
|
406
|
+
const re = /(?:^|[\s;{}])(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(([^)]*)\)\s*\{/g;
|
|
407
|
+
let match: RegExpExecArray | null;
|
|
408
|
+
let candidate: { name: string; params: string[]; bodyStart: number; bodyEnd: number } | null =
|
|
409
|
+
null;
|
|
410
|
+
while ((match = re.exec(before)) !== null) {
|
|
411
|
+
const kwOffset = match[0].search(/async|function/);
|
|
412
|
+
if (!isStatementFunctionHead(source, match.index + kwOffset)) continue;
|
|
413
|
+
const bodyOpen = match.index + match[0].length;
|
|
414
|
+
const close = matchClosingBrace(source, bodyOpen);
|
|
415
|
+
if (close > pos) {
|
|
416
|
+
candidate = {
|
|
417
|
+
name: match[1]!,
|
|
418
|
+
params: paramNames(match[2]!),
|
|
419
|
+
bodyStart: bodyOpen,
|
|
420
|
+
bodyEnd: close,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return candidate;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Identifiers declared in the enclosing function that a captured arrow may close over: the function's
|
|
429
|
+
* params plus its local function/const/let/var declarations. Over-collection is harmless - only names
|
|
430
|
+
* actually referenced in the closure body become captures.
|
|
431
|
+
*/
|
|
432
|
+
function enclosingCaptureCandidates(
|
|
433
|
+
source: string,
|
|
434
|
+
enclosing: { params: string[]; bodyStart: number; bodyEnd: number },
|
|
435
|
+
): string[] {
|
|
436
|
+
const names = new Set(enclosing.params);
|
|
437
|
+
const body = source.slice(enclosing.bodyStart, enclosing.bodyEnd);
|
|
438
|
+
for (const match of body.matchAll(/\bfunction\s*\*?\s*([A-Za-z_$][\w$]*)/g)) {
|
|
439
|
+
if (match[1]) names.add(match[1]);
|
|
440
|
+
}
|
|
441
|
+
for (const match of body.matchAll(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)/g)) {
|
|
442
|
+
if (match[1]) names.add(match[1]);
|
|
443
|
+
}
|
|
444
|
+
return [...names];
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
interface NestedClosure {
|
|
448
|
+
name: string;
|
|
449
|
+
params: string;
|
|
450
|
+
asyncKeyword: string;
|
|
451
|
+
declStart: number;
|
|
452
|
+
bodyEnd: number;
|
|
453
|
+
enclosingName: string;
|
|
454
|
+
captures: string[];
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Given the index of a `'use cache'` directive that opens a function body, find the immediately
|
|
459
|
+
* enclosing named function declaration nested inside another named function and compute its captured
|
|
460
|
+
* enclosing-parameter identifiers. Returns null when the directive does not open one - object methods,
|
|
461
|
+
* statics and arrows are left inert, which is safe: they run uncached rather than breaking the build.
|
|
462
|
+
*/
|
|
463
|
+
function findNestedClosure(source: string, directiveIndex: number): NestedClosure | null {
|
|
464
|
+
const prefix = source.slice(0, directiveIndex);
|
|
465
|
+
// Match the nearest `[async ]function NAME(params) {` whose brace opens the
|
|
466
|
+
// directive body (exactly one unclosed brace between it and the directive).
|
|
467
|
+
const declRe = /(?:^|[\s;{}])(async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(([^)]*)\)\s*\{/g;
|
|
468
|
+
let match: RegExpExecArray | null;
|
|
469
|
+
let best: { asyncKeyword: string; name: string; params: string; headStart: number; bodyOpen: number } | null =
|
|
470
|
+
null;
|
|
471
|
+
while ((match = declRe.exec(prefix)) !== null) {
|
|
472
|
+
// Clean head start: the position of the `async`/`function` keyword (the
|
|
473
|
+
// match may include a leading whitespace/`{`/`;` separator char).
|
|
474
|
+
const kwOffset = match[0].search(/async|function/);
|
|
475
|
+
const cleanStart = match.index + kwOffset;
|
|
476
|
+
if (!isStatementFunctionHead(source, cleanStart)) continue;
|
|
477
|
+
const bodyOpen = match.index + match[0].length; // just after `{`
|
|
478
|
+
// Balance braces from bodyOpen to directiveIndex — must be exactly 0 open
|
|
479
|
+
// *within* this function head region, i.e. the directive is the first stmt.
|
|
480
|
+
const between = source.slice(bodyOpen, directiveIndex);
|
|
481
|
+
let balance = 0;
|
|
482
|
+
for (const ch of between) {
|
|
483
|
+
if (ch === '{') balance += 1;
|
|
484
|
+
else if (ch === '}') balance -= 1;
|
|
485
|
+
}
|
|
486
|
+
if (balance === 0) {
|
|
487
|
+
if (match[2] === 'function') continue;
|
|
488
|
+
best = {
|
|
489
|
+
asyncKeyword: match[1] ? 'async ' : '',
|
|
490
|
+
name: match[2]!,
|
|
491
|
+
params: match[3]!.trim(),
|
|
492
|
+
headStart: cleanStart,
|
|
493
|
+
bodyOpen,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (!best) return null;
|
|
498
|
+
|
|
499
|
+
// The closure must be genuinely nested: there is an enclosing named function
|
|
500
|
+
// whose body contains `best`. Find the nearest such enclosing declaration.
|
|
501
|
+
const enclosing = findEnclosingNamedFunction(source, best.headStart);
|
|
502
|
+
if (!enclosing) return null;
|
|
503
|
+
|
|
504
|
+
// Find the matching close brace of the closure body.
|
|
505
|
+
const bodyEnd = matchClosingBrace(source, best.bodyOpen);
|
|
506
|
+
if (bodyEnd < 0) return null;
|
|
507
|
+
const body = source.slice(best.bodyOpen, bodyEnd);
|
|
508
|
+
|
|
509
|
+
// Captures: enclosing params referenced (whole-word) in the closure body and
|
|
510
|
+
// not shadowed by the closure's own params.
|
|
511
|
+
const closureParams = new Set(paramNames(best.params));
|
|
512
|
+
const captures = enclosing.params.filter(
|
|
513
|
+
param => !closureParams.has(param) && new RegExp(`\\b${escapeIdent(param)}\\b`).test(body),
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
name: best.name,
|
|
518
|
+
params: best.params,
|
|
519
|
+
asyncKeyword: best.asyncKeyword,
|
|
520
|
+
declStart: best.headStart,
|
|
521
|
+
bodyEnd,
|
|
522
|
+
enclosingName: enclosing.name,
|
|
523
|
+
captures,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** Nearest enclosing `[async ]function NAME(params)` whose body wraps `pos`. */
|
|
528
|
+
function findEnclosingNamedFunction(
|
|
529
|
+
source: string,
|
|
530
|
+
pos: number,
|
|
531
|
+
): { name: string; params: string[] } | null {
|
|
532
|
+
const before = source.slice(0, pos);
|
|
533
|
+
const re = /(?:^|[\s;{}])(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(([^)]*)\)\s*\{/g;
|
|
534
|
+
let match: RegExpExecArray | null;
|
|
535
|
+
let candidate: { name: string; params: string[] } | null = null;
|
|
536
|
+
while ((match = re.exec(before)) !== null) {
|
|
537
|
+
const kwOffset = match[0].search(/async|function/);
|
|
538
|
+
if (!isStatementFunctionHead(source, match.index + kwOffset)) continue;
|
|
539
|
+
const bodyOpen = match.index + match[0].length;
|
|
540
|
+
const close = matchClosingBrace(source, bodyOpen);
|
|
541
|
+
if (close > pos) {
|
|
542
|
+
candidate = { name: match[1]!, params: paramNames(match[2]!) };
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return candidate;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function isStatementFunctionHead(source: string, index: number): boolean {
|
|
549
|
+
if (/(^|[\s;{}])export\s+(?:default\s+)?$/.test(source.slice(0, index))) return true;
|
|
550
|
+
let previous = index - 1;
|
|
551
|
+
while (previous >= 0 && /\s/.test(source[previous]!)) previous -= 1;
|
|
552
|
+
if (previous < 0) return true;
|
|
553
|
+
if (source[previous] === '=') return false;
|
|
554
|
+
if (source[previous] === '{') {
|
|
555
|
+
let beforeBrace = previous - 1;
|
|
556
|
+
while (beforeBrace >= 0 && /\s/.test(source[beforeBrace]!)) beforeBrace -= 1;
|
|
557
|
+
return source[beforeBrace] !== '=';
|
|
558
|
+
}
|
|
559
|
+
return source[previous] === ';' || source[previous] === '}';
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** Index just past the `}` matching the `{` at `openIndex`, or -1. */
|
|
563
|
+
function matchClosingBrace(source: string, openIndex: number): number {
|
|
564
|
+
let balance = 1;
|
|
565
|
+
for (let i = openIndex; i < source.length; i += 1) {
|
|
566
|
+
const ch = source[i];
|
|
567
|
+
if (ch === '{') balance += 1;
|
|
568
|
+
else if (ch === '}') {
|
|
569
|
+
balance -= 1;
|
|
570
|
+
if (balance === 0) return i;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return -1;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function escapeIdent(name: string): string {
|
|
577
|
+
return name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/** Extract simple parameter identifier names from a `(...)` param string. */
|
|
581
|
+
function paramNames(params: string): string[] {
|
|
582
|
+
const names: string[] = [];
|
|
583
|
+
for (const part of params.split(',')) {
|
|
584
|
+
const name = part.trim().split(/[=:\s]/)[0];
|
|
585
|
+
if (name) names.push(name);
|
|
586
|
+
}
|
|
587
|
+
return names;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Module-level directive: strip it and wrap every exported function-ish
|
|
592
|
+
* declaration. `export const` becomes `export let` so the binding is
|
|
593
|
+
* reassignable.
|
|
594
|
+
*/
|
|
595
|
+
function rewriteModuleLevelDirective(source: string, wrapped: Set<string>): string {
|
|
596
|
+
const prologue = /^(?:#![^\n]*\n)?(?:\s|\/\/[^\n]*\n|\/\*[\s\S]*?\*\/)*/.exec(source);
|
|
597
|
+
const offset = prologue?.[0].length ?? 0;
|
|
598
|
+
const head = source.slice(offset);
|
|
599
|
+
const directive = /^(['"])use cache(?:\s*:\s*[\w-]+)?\1\s*;?/.exec(head);
|
|
600
|
+
if (!directive) return source;
|
|
601
|
+
|
|
602
|
+
let out = source.slice(0, offset) + head.slice(directive[0].length);
|
|
603
|
+
|
|
604
|
+
// export [async] function NAME / export [default] [async] function NAME
|
|
605
|
+
for (const match of out.matchAll(
|
|
606
|
+
/\bexport\s+(?:default\s+)?(?:async\s+)?function\s*\*?\s+([A-Za-z_$][\w$]*)/g,
|
|
607
|
+
)) {
|
|
608
|
+
if (match[1]) wrapped.add(match[1]);
|
|
609
|
+
}
|
|
610
|
+
// export const|let|var NAME = ... (assume function values; const -> let)
|
|
611
|
+
out = out.replace(
|
|
612
|
+
/\bexport\s+(const|let|var)\s+([A-Za-z_$][\w$]*)\s*=/g,
|
|
613
|
+
(full, keyword: string, name: string) => {
|
|
614
|
+
wrapped.add(name);
|
|
615
|
+
return full.replace(keyword, keyword === 'const' ? 'let' : keyword);
|
|
616
|
+
},
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
// `const NAME = ...; export { NAME }` — a re-exported binding under a
|
|
620
|
+
// module-level directive. Wrap each re-exported name declared as a top-level
|
|
621
|
+
// const/let/var function value (flip its const to let so it's reassignable).
|
|
622
|
+
const reExported = new Set<string>();
|
|
623
|
+
for (const clause of out.matchAll(/\bexport\s*\{([^}]*)\}/g)) {
|
|
624
|
+
for (const spec of clause[1]!.split(',')) {
|
|
625
|
+
const local = spec.trim().split(/\s+as\s+/)[0]?.trim();
|
|
626
|
+
if (local && /^[A-Za-z_$][\w$]*$/.test(local)) reExported.add(local);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
for (const name of reExported) {
|
|
630
|
+
if (wrapped.has(name)) continue;
|
|
631
|
+
const declRe = new RegExp(`(^|[\\n;])\\s*(const|let|var)\\s+${escapeIdent(name)}\\s*=`);
|
|
632
|
+
const declMatch = declRe.exec(out);
|
|
633
|
+
if (!declMatch) continue;
|
|
634
|
+
wrapped.add(name);
|
|
635
|
+
if (declMatch[2] === 'const') {
|
|
636
|
+
out =
|
|
637
|
+
out.slice(0, declMatch.index) +
|
|
638
|
+
declMatch[0].replace(/\bconst\b/, 'let') +
|
|
639
|
+
out.slice(declMatch.index + declMatch[0].length);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
for (const match of out.matchAll(/\bexport\s+default\s+([A-Za-z_$][\w$]*)\s*;?/g)) {
|
|
644
|
+
const name = match[1]!;
|
|
645
|
+
if (wrapped.has(name)) continue;
|
|
646
|
+
const declRe = new RegExp(`(^|[\\n;])\\s*(const|let|var)\\s+${escapeIdent(name)}\\s*=`);
|
|
647
|
+
const declMatch = declRe.exec(out);
|
|
648
|
+
if (!declMatch) continue;
|
|
649
|
+
wrapped.add(name);
|
|
650
|
+
if (declMatch[2] === 'const') {
|
|
651
|
+
out =
|
|
652
|
+
out.slice(0, declMatch.index) +
|
|
653
|
+
declMatch[0].replace(/\bconst\b/, 'let') +
|
|
654
|
+
out.slice(declMatch.index + declMatch[0].length);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return out;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function rewriteObjectMethods(source: string, idBase: string): { source: string; wrapped: boolean } {
|
|
661
|
+
let out = source;
|
|
662
|
+
let wrapped = false;
|
|
663
|
+
let searchFrom = 0;
|
|
664
|
+
for (;;) {
|
|
665
|
+
const tail = out.slice(searchFrom);
|
|
666
|
+
const directive = directiveText.exec(tail);
|
|
667
|
+
if (!directive) break;
|
|
668
|
+
const index = searchFrom + directive.index;
|
|
669
|
+
if (!/\{\s*$/.test(out.slice(0, index))) {
|
|
670
|
+
searchFrom = index + directive[0].length;
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
const method = findObjectMethod(out, index);
|
|
674
|
+
if (!method) {
|
|
675
|
+
searchFrom = index + directive[0].length;
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
const id = `${idBase}#${method.enclosingName}.${method.name}`;
|
|
679
|
+
const captureList = method.captures.join(', ');
|
|
680
|
+
const wrappedHead = `${method.name}: ${boundHelper}(${JSON.stringify(id)}, ${method.asyncKeyword}function ${method.name}(${method.params}) {`;
|
|
681
|
+
out =
|
|
682
|
+
out.slice(0, method.declStart) +
|
|
683
|
+
wrappedHead +
|
|
684
|
+
out.slice(index + directive[0].length, method.bodyEnd) +
|
|
685
|
+
`}, () => [${captureList}])` +
|
|
686
|
+
out.slice(method.bodyEnd + 1);
|
|
687
|
+
wrapped = true;
|
|
688
|
+
searchFrom = method.declStart + wrappedHead.length;
|
|
689
|
+
}
|
|
690
|
+
return { source: out, wrapped };
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function findObjectMethod(source: string, directiveIndex: number): NestedClosure | null {
|
|
694
|
+
const prefix = source.slice(0, directiveIndex);
|
|
695
|
+
const methodRe = /(?:^|[,{]\s*)(async\s+)?([A-Za-z_$][\w$]*)\s*\(([^)]*)\)\s*\{/g;
|
|
696
|
+
let match: RegExpExecArray | null;
|
|
697
|
+
let best: { asyncKeyword: string; name: string; params: string; headStart: number; bodyOpen: number } | null =
|
|
698
|
+
null;
|
|
699
|
+
while ((match = methodRe.exec(prefix)) !== null) {
|
|
700
|
+
let beforeMatch = match.index - 1;
|
|
701
|
+
while (beforeMatch >= 0 && /\s/.test(source[beforeMatch]!)) beforeMatch -= 1;
|
|
702
|
+
if (source[beforeMatch] === '=') continue;
|
|
703
|
+
const kwOffset = match[0].search(/async|[A-Za-z_$]/);
|
|
704
|
+
const cleanStart = match.index + kwOffset;
|
|
705
|
+
const bodyOpen = match.index + match[0].length;
|
|
706
|
+
const between = source.slice(bodyOpen, directiveIndex);
|
|
707
|
+
let balance = 0;
|
|
708
|
+
for (const ch of between) {
|
|
709
|
+
if (ch === '{') balance += 1;
|
|
710
|
+
else if (ch === '}') balance -= 1;
|
|
711
|
+
}
|
|
712
|
+
if (balance === 0) {
|
|
713
|
+
best = {
|
|
714
|
+
asyncKeyword: match[1] ? 'async ' : '',
|
|
715
|
+
name: match[2]!,
|
|
716
|
+
params: match[3]!.trim(),
|
|
717
|
+
headStart: cleanStart,
|
|
718
|
+
bodyOpen,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if (!best) return null;
|
|
723
|
+
const enclosing = findEnclosingNamedFunction(source, best.headStart);
|
|
724
|
+
if (!enclosing) return null;
|
|
725
|
+
const bodyEnd = matchClosingBrace(source, best.bodyOpen);
|
|
726
|
+
if (bodyEnd < 0) return null;
|
|
727
|
+
const body = source.slice(best.bodyOpen, bodyEnd);
|
|
728
|
+
const methodParams = new Set(paramNames(best.params));
|
|
729
|
+
const captures = enclosing.params.filter(
|
|
730
|
+
param => !methodParams.has(param) && new RegExp(`\\b${escapeIdent(param)}\\b`).test(body),
|
|
731
|
+
);
|
|
732
|
+
return {
|
|
733
|
+
name: best.name,
|
|
734
|
+
params: best.params,
|
|
735
|
+
asyncKeyword: best.asyncKeyword,
|
|
736
|
+
declStart: best.headStart,
|
|
737
|
+
bodyEnd,
|
|
738
|
+
enclosingName: enclosing.name,
|
|
739
|
+
captures,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function rewriteStaticClassMethods(source: string, idBase: string): { source: string; wrapped: boolean } {
|
|
744
|
+
let out = source;
|
|
745
|
+
let wrapped = false;
|
|
746
|
+
let searchFrom = 0;
|
|
747
|
+
for (;;) {
|
|
748
|
+
const tail = out.slice(searchFrom);
|
|
749
|
+
const directive = directiveText.exec(tail);
|
|
750
|
+
if (!directive) break;
|
|
751
|
+
const index = searchFrom + directive.index;
|
|
752
|
+
if (!/\{\s*$/.test(out.slice(0, index))) {
|
|
753
|
+
searchFrom = index + directive[0].length;
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
const method = findStaticClassMethod(out, index);
|
|
757
|
+
if (!method) {
|
|
758
|
+
searchFrom = index + directive[0].length;
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
const id = `${idBase}#${method.className}.${method.name}`;
|
|
762
|
+
const wrappedHead = `static ${method.name} = __pnextUseCache(${JSON.stringify(id)}, ${method.asyncKeyword}function ${method.name}(${method.params}) {`;
|
|
763
|
+
out =
|
|
764
|
+
out.slice(0, method.declStart) +
|
|
765
|
+
wrappedHead +
|
|
766
|
+
out.slice(index + directive[0].length, method.bodyEnd) +
|
|
767
|
+
`})` +
|
|
768
|
+
out.slice(method.bodyEnd + 1);
|
|
769
|
+
wrapped = true;
|
|
770
|
+
searchFrom = method.declStart + wrappedHead.length;
|
|
771
|
+
}
|
|
772
|
+
return { source: out, wrapped };
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function findStaticClassMethod(
|
|
776
|
+
source: string,
|
|
777
|
+
directiveIndex: number,
|
|
778
|
+
): (NestedClosure & { className: string }) | null {
|
|
779
|
+
const prefix = source.slice(0, directiveIndex);
|
|
780
|
+
const methodRe = /(?:^|[\s;{}])static\s+(async\s+)?([A-Za-z_$][\w$]*)\s*\(([^)]*)\)\s*\{/g;
|
|
781
|
+
let match: RegExpExecArray | null;
|
|
782
|
+
let best: { asyncKeyword: string; name: string; params: string; headStart: number; bodyOpen: number } | null =
|
|
783
|
+
null;
|
|
784
|
+
while ((match = methodRe.exec(prefix)) !== null) {
|
|
785
|
+
const kwOffset = match[0].indexOf('static');
|
|
786
|
+
const cleanStart = match.index + kwOffset;
|
|
787
|
+
const bodyOpen = match.index + match[0].length;
|
|
788
|
+
const between = source.slice(bodyOpen, directiveIndex);
|
|
789
|
+
let balance = 0;
|
|
790
|
+
for (const ch of between) {
|
|
791
|
+
if (ch === '{') balance += 1;
|
|
792
|
+
else if (ch === '}') balance -= 1;
|
|
793
|
+
}
|
|
794
|
+
if (balance === 0) {
|
|
795
|
+
best = {
|
|
796
|
+
asyncKeyword: match[1] ? 'async ' : '',
|
|
797
|
+
name: match[2]!,
|
|
798
|
+
params: match[3]!.trim(),
|
|
799
|
+
headStart: cleanStart,
|
|
800
|
+
bodyOpen,
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
if (!best) return null;
|
|
805
|
+
const className = findEnclosingClassName(source, best.headStart);
|
|
806
|
+
if (!className) return null;
|
|
807
|
+
const bodyEnd = matchClosingBrace(source, best.bodyOpen);
|
|
808
|
+
if (bodyEnd < 0) return null;
|
|
809
|
+
return {
|
|
810
|
+
name: best.name,
|
|
811
|
+
params: best.params,
|
|
812
|
+
asyncKeyword: best.asyncKeyword,
|
|
813
|
+
declStart: best.headStart,
|
|
814
|
+
bodyEnd,
|
|
815
|
+
enclosingName: className,
|
|
816
|
+
className,
|
|
817
|
+
captures: [],
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function findEnclosingClassName(source: string, pos: number): string | null {
|
|
822
|
+
const before = source.slice(0, pos);
|
|
823
|
+
const re = /(?:^|[\s;{}])class\s+([A-Za-z_$][\w$]*)[^{]*\{/g;
|
|
824
|
+
let match: RegExpExecArray | null;
|
|
825
|
+
let candidate: string | null = null;
|
|
826
|
+
while ((match = re.exec(before)) !== null) {
|
|
827
|
+
const bodyOpen = match.index + match[0].length;
|
|
828
|
+
const close = matchClosingBrace(source, bodyOpen);
|
|
829
|
+
if (close > pos) candidate = match[1]!;
|
|
830
|
+
}
|
|
831
|
+
return candidate;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Function-level directives: for each `'use cache'` opening a function body,
|
|
836
|
+
* locate the enclosing declaration's name by scanning backwards, verify the
|
|
837
|
+
* region between the declaration head and the directive is a plausible
|
|
838
|
+
* function signature (brace balance of exactly one open), strip the directive
|
|
839
|
+
* and mark the name for wrapping.
|
|
840
|
+
*/
|
|
841
|
+
function rewriteFunctionLevelDirectives(
|
|
842
|
+
source: string,
|
|
843
|
+
wrapped: Set<string>,
|
|
844
|
+
privateWrapped: Set<string>,
|
|
845
|
+
): string {
|
|
846
|
+
let out = source;
|
|
847
|
+
let searchFrom = 0;
|
|
848
|
+
for (;;) {
|
|
849
|
+
const tail = out.slice(searchFrom);
|
|
850
|
+
const directive = directiveText.exec(tail);
|
|
851
|
+
if (!directive) break;
|
|
852
|
+
const index = searchFrom + directive.index;
|
|
853
|
+
const prefix = out.slice(0, index);
|
|
854
|
+
|
|
855
|
+
// Only a directive when it opens a function body; anything else is a plain
|
|
856
|
+
// string literal.
|
|
857
|
+
const declaration = opensFunctionBody(prefix) ? findEnclosingDeclaration(prefix) : null;
|
|
858
|
+
if (!declaration) {
|
|
859
|
+
searchFrom = index + directive[0].length;
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
let head = prefix;
|
|
864
|
+
if (declaration.constKeywordIndex !== undefined) {
|
|
865
|
+
head =
|
|
866
|
+
prefix.slice(0, declaration.constKeywordIndex) +
|
|
867
|
+
'let ' +
|
|
868
|
+
prefix.slice(declaration.constKeywordIndex + 'const'.length);
|
|
869
|
+
}
|
|
870
|
+
wrapped.add(declaration.name);
|
|
871
|
+
if (directive[2] === 'private') privateWrapped.add(declaration.name);
|
|
872
|
+
out = head + out.slice(index + directive[0].length);
|
|
873
|
+
searchFrom = index;
|
|
874
|
+
}
|
|
875
|
+
return out;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Whether `prefix` ends at the start of a function body - the brace reached by skipping back over
|
|
880
|
+
* whitespace AND comments. A directive prologue is the first STATEMENT of the body, so a comment above
|
|
881
|
+
* `'use cache'` must not hide it. Scanned rather than matched: a regex alternating whitespace and
|
|
882
|
+
* comment forms backtracks badly.
|
|
883
|
+
*/
|
|
884
|
+
function opensFunctionBody(prefix: string): boolean {
|
|
885
|
+
// The match is a comment's own text, not a directive, when `prefix` ends
|
|
886
|
+
// inside a comment — e.g. a line explaining `'use cache'` semantics.
|
|
887
|
+
if (prefix.includes('//', prefix.lastIndexOf('\n') + 1)) return false;
|
|
888
|
+
const blockOpen = prefix.lastIndexOf('/*');
|
|
889
|
+
if (blockOpen !== -1 && !prefix.includes('*/', blockOpen)) return false;
|
|
890
|
+
|
|
891
|
+
let end = prefix.length;
|
|
892
|
+
for (;;) {
|
|
893
|
+
end = prefix.slice(0, end).replace(/\s+$/, '').length;
|
|
894
|
+
if (end === 0) return false;
|
|
895
|
+
if (prefix.startsWith('*/', end - 2)) {
|
|
896
|
+
const open = prefix.lastIndexOf('/*', end - 2);
|
|
897
|
+
if (open === -1) return false;
|
|
898
|
+
end = open;
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
const comment = prefix.indexOf('//', prefix.lastIndexOf('\n', end - 1) + 1);
|
|
902
|
+
if (comment !== -1 && comment < end) {
|
|
903
|
+
end = comment;
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
return prefix[end - 1] === '{';
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
interface EnclosingDeclaration {
|
|
911
|
+
name: string;
|
|
912
|
+
/** Index of a `const` keyword to rewrite to `let`, when applicable. */
|
|
913
|
+
constKeywordIndex?: number;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function findEnclosingDeclaration(prefix: string): EnclosingDeclaration | null {
|
|
917
|
+
const candidates: { name: string; end: number; constKeywordIndex?: number }[] = [];
|
|
918
|
+
|
|
919
|
+
// Only top-level declarations (line starts at column 0): a nested closure's
|
|
920
|
+
// binding is function-scoped and cannot be reassigned from module level —
|
|
921
|
+
// those run uncached (the directive string is inert).
|
|
922
|
+
const topLevel = (index: number) => index === 0 || prefix[index - 1] === '\n';
|
|
923
|
+
|
|
924
|
+
const fnPattern = /(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s+([A-Za-z_$][\w$]*)/g;
|
|
925
|
+
for (const match of prefix.matchAll(fnPattern)) {
|
|
926
|
+
if (match[1] && topLevel(match.index)) {
|
|
927
|
+
candidates.push({ name: match[1], end: match.index + match[0].length });
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
const bindingPattern = /(?:export\s+)?(const|let|var)\s+([A-Za-z_$][\w$]*)\s*=/g;
|
|
931
|
+
for (const match of prefix.matchAll(bindingPattern)) {
|
|
932
|
+
if (!match[2] || !topLevel(match.index)) continue;
|
|
933
|
+
candidates.push({
|
|
934
|
+
name: match[2],
|
|
935
|
+
end: match.index + match[0].length,
|
|
936
|
+
...(match[1] === 'const'
|
|
937
|
+
? { constKeywordIndex: match.index + match[0].indexOf('const') }
|
|
938
|
+
: {}),
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
if (candidates.length === 0) return null;
|
|
942
|
+
candidates.sort((a, b) => a.end - b.end);
|
|
943
|
+
const nearest = candidates[candidates.length - 1]!;
|
|
944
|
+
|
|
945
|
+
// The span between the declaration head and the directive must look like a
|
|
946
|
+
// signature followed by the opening body brace: exactly one unclosed '{'.
|
|
947
|
+
const span = prefix.slice(nearest.end);
|
|
948
|
+
let balance = 0;
|
|
949
|
+
for (const char of span) {
|
|
950
|
+
if (char === '{') balance += 1;
|
|
951
|
+
else if (char === '}') balance -= 1;
|
|
952
|
+
}
|
|
953
|
+
if (balance !== 1) return null;
|
|
954
|
+
|
|
955
|
+
return {
|
|
956
|
+
name: nearest.name,
|
|
957
|
+
...(nearest.constKeywordIndex !== undefined
|
|
958
|
+
? { constKeywordIndex: nearest.constKeywordIndex }
|
|
959
|
+
: {}),
|
|
960
|
+
};
|
|
961
|
+
}
|