@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,307 @@
|
|
|
1
|
+
import { rewriteFacts } from '../resolve/scan-facts';
|
|
2
|
+
|
|
3
|
+
export interface DynamicCall {
|
|
4
|
+
index: number;
|
|
5
|
+
name: string;
|
|
6
|
+
open: number;
|
|
7
|
+
close: number;
|
|
8
|
+
source: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Cheap gate: a `dynamic` binding can only come from one of these specifiers. */
|
|
12
|
+
export function sourceHasDynamicImport(source: string) {
|
|
13
|
+
return source.includes('@wular/pnext') || source.includes('next/dynamic');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Local names bound to `dynamic()`. The import edge is module-record-shaped and
|
|
18
|
+
* comes off the memoized parse; only the call-site
|
|
19
|
+
* scanning below stays textual.
|
|
20
|
+
*/
|
|
21
|
+
export function pnextDynamicImportNames(source: string, file = 'module.tsx') {
|
|
22
|
+
const names = new Set<string>();
|
|
23
|
+
if (!sourceHasDynamicImport(source)) return names;
|
|
24
|
+
for (const statement of rewriteFacts(file, source).imports) {
|
|
25
|
+
const { specifier } = statement;
|
|
26
|
+
const pnext = specifier === '@wular/pnext' || specifier === '@wular/pnext/dynamic';
|
|
27
|
+
// The compat alias rewrite may already have turned `next/dynamic` into the
|
|
28
|
+
// shim's own file path/href before this scan runs.
|
|
29
|
+
const nextDynamic =
|
|
30
|
+
specifier === 'next/dynamic' || /compat\/next\/dynamic(?:\.tsx)?$/.test(specifier);
|
|
31
|
+
if (!pnext && !nextDynamic) continue;
|
|
32
|
+
for (const binding of statement.bindings) {
|
|
33
|
+
if (binding.type) continue;
|
|
34
|
+
if (pnext ? binding.imported === 'dynamic' : binding.imported === 'default') {
|
|
35
|
+
names.add(binding.local);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return names;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function dynamicCallsFromSource(source: string, dynamicNames: Set<string>) {
|
|
43
|
+
const calls: DynamicCall[] = [];
|
|
44
|
+
if (dynamicNames.size === 0) return calls;
|
|
45
|
+
|
|
46
|
+
let index = 0;
|
|
47
|
+
|
|
48
|
+
while ((index = nextDynamicCallIndex(source, dynamicNames, index)) !== -1) {
|
|
49
|
+
const name = dynamicNameAt(source, dynamicNames, index);
|
|
50
|
+
if (!name) {
|
|
51
|
+
index += 1;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const before = source[index - 1];
|
|
55
|
+
const after = source[index + name.length];
|
|
56
|
+
if ((before && /[\w$]/.test(before)) || (after && /[\w$]/.test(after))) {
|
|
57
|
+
index += name.length;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const open = source.indexOf('(', index + name.length);
|
|
62
|
+
if (open === -1 || source.slice(index + name.length, open).trim()) {
|
|
63
|
+
index += name.length;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const close = matchingParen(source, open);
|
|
68
|
+
if (close === -1) {
|
|
69
|
+
index = open + 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
calls.push({
|
|
74
|
+
index,
|
|
75
|
+
name,
|
|
76
|
+
open,
|
|
77
|
+
close,
|
|
78
|
+
source: source.slice(index, close + 1),
|
|
79
|
+
});
|
|
80
|
+
index = close + 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return calls;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function rewriteLiteralDynamicCalls(source: string, file?: string) {
|
|
87
|
+
const dynamicNames = pnextDynamicImportNames(source, file);
|
|
88
|
+
if (dynamicNames.size === 0) return source;
|
|
89
|
+
|
|
90
|
+
const edits: { start: number; end: number; value: string }[] = [];
|
|
91
|
+
for (const call of dynamicCallsFromSource(source, dynamicNames)) {
|
|
92
|
+
const args = source.slice(call.open + 1, call.close);
|
|
93
|
+
const literal = /^(\s*)(['"])([^'"]+)\2(?=\s*(?:,|$))/.exec(args);
|
|
94
|
+
if (!literal) continue;
|
|
95
|
+
const [match, leading = '', quote = "'", specifier] = literal;
|
|
96
|
+
if (!specifier) continue;
|
|
97
|
+
const start = call.open + 1 + leading.length;
|
|
98
|
+
edits.push({
|
|
99
|
+
start,
|
|
100
|
+
end: call.open + 1 + match.length,
|
|
101
|
+
value: `() => import(${quote}${specifier}${quote})`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (edits.length === 0) return source;
|
|
106
|
+
|
|
107
|
+
let next = source;
|
|
108
|
+
for (const edit of edits.reverse()) {
|
|
109
|
+
next = `${next.slice(0, edit.start)}${edit.value}${next.slice(edit.end)}`;
|
|
110
|
+
}
|
|
111
|
+
return next;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Server compiles append the statically-resolved import target as a third
|
|
115
|
+
// dynamic() argument, so the renderer can match the call to its scanned client
|
|
116
|
+
// reference without importing the module. Instance identity is useless for
|
|
117
|
+
// that match: dev route bundles, per-module compiles, and the client-SSR
|
|
118
|
+
// profile each produce their own copy of the target module.
|
|
119
|
+
export function rewriteDynamicCallTargets(
|
|
120
|
+
source: string,
|
|
121
|
+
resolve: (specifier: string) => string | undefined,
|
|
122
|
+
file?: string,
|
|
123
|
+
) {
|
|
124
|
+
const dynamicNames = pnextDynamicImportNames(source, file);
|
|
125
|
+
if (dynamicNames.size === 0) return source;
|
|
126
|
+
|
|
127
|
+
const edits: { at: number; value: string }[] = [];
|
|
128
|
+
for (const call of dynamicCallsFromSource(source, dynamicNames)) {
|
|
129
|
+
const target = dynamicImportTarget.exec(call.source);
|
|
130
|
+
if (!target?.[2]) continue;
|
|
131
|
+
const file = resolve(target[2]);
|
|
132
|
+
if (!file) continue;
|
|
133
|
+
|
|
134
|
+
const args = source.slice(call.open + 1, call.close);
|
|
135
|
+
const argCount = topLevelArgCount(args);
|
|
136
|
+
if (argCount < 1 || argCount > 2) continue;
|
|
137
|
+
const literal = JSON.stringify({ file, exportName: target[4] ?? 'default' });
|
|
138
|
+
const separator = /,\s*$/.test(args) ? '' : ', ';
|
|
139
|
+
edits.push({
|
|
140
|
+
at: call.close,
|
|
141
|
+
value: `${separator}${argCount === 1 ? 'undefined, ' : ''}${literal}`,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (edits.length === 0) return source;
|
|
146
|
+
|
|
147
|
+
let next = source;
|
|
148
|
+
for (const edit of edits.reverse()) {
|
|
149
|
+
next = `${next.slice(0, edit.at)}${edit.value}${next.slice(edit.at)}`;
|
|
150
|
+
}
|
|
151
|
+
return next;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const dynamicImportTarget =
|
|
155
|
+
/import\s*\(\s*(['"])([^'"]+)\1\s*\)(?:\s*\.then\s*\(\s*([A-Za-z_$][\w$]*)\s*=>\s*\3\.([A-Za-z_$][\w$]*)\s*\))?/;
|
|
156
|
+
|
|
157
|
+
/** Dev split of `dynamic(ssr:false)` targets; `PNEXT_DYNAMIC_SPLIT=0` restores eager builds. */
|
|
158
|
+
export function devDynamicSplitEnabled() {
|
|
159
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
160
|
+
return process.env.PNEXT_DYNAMIC_SPLIT !== '0';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Rewrite each deferred dynamic() import in a CLIENT-BUILD source to the served
|
|
165
|
+
* chunk URL `href` names, so the entry build never bundles the target's graph.
|
|
166
|
+
* Callers register the (file, exportName) behind each URL as `href` is asked.
|
|
167
|
+
*/
|
|
168
|
+
export function rewriteDeferredDynamicImports(
|
|
169
|
+
source: string,
|
|
170
|
+
file: string | undefined,
|
|
171
|
+
resolve: (specifier: string) => string | undefined,
|
|
172
|
+
href: (target: { file: string; exportName: string }) => string | undefined,
|
|
173
|
+
) {
|
|
174
|
+
const dynamicNames = pnextDynamicImportNames(source, file);
|
|
175
|
+
if (dynamicNames.size === 0) return source;
|
|
176
|
+
const deferred = deferredDynamicImportSpecifiers(source, file);
|
|
177
|
+
if (deferred.size === 0) return source;
|
|
178
|
+
|
|
179
|
+
const edits: { start: number; end: number; value: string }[] = [];
|
|
180
|
+
for (const call of dynamicCallsFromSource(source, dynamicNames)) {
|
|
181
|
+
const target = dynamicImportTarget.exec(call.source);
|
|
182
|
+
if (!target?.[2] || !deferred.has(target[2])) continue;
|
|
183
|
+
const resolved = resolve(target[2]);
|
|
184
|
+
if (!resolved) continue;
|
|
185
|
+
const url = href({ file: resolved, exportName: target[4] ?? 'default' });
|
|
186
|
+
if (!url) continue;
|
|
187
|
+
const specifierAt = call.index + target.index + target[0].indexOf(target[2]);
|
|
188
|
+
edits.push({ start: specifierAt, end: specifierAt + target[2].length, value: url });
|
|
189
|
+
}
|
|
190
|
+
if (edits.length === 0) return source;
|
|
191
|
+
let next = source;
|
|
192
|
+
for (const edit of edits.sort((a, b) => b.start - a.start)) {
|
|
193
|
+
next = `${next.slice(0, edit.start)}${edit.value}${next.slice(edit.end)}`;
|
|
194
|
+
}
|
|
195
|
+
return next;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Import specifiers of `dynamic()` calls the SSR layer never executes (`ssr: false`, or `load: 'visible'`
|
|
200
|
+
* without `ssr: true`) - the dev split points: their target modules compile on browser demand, not eagerly.
|
|
201
|
+
*/
|
|
202
|
+
export function deferredDynamicImportSpecifiers(source: string, file?: string) {
|
|
203
|
+
const deferred = new Set<string>();
|
|
204
|
+
const dynamicNames = pnextDynamicImportNames(source, file);
|
|
205
|
+
if (dynamicNames.size === 0) return deferred;
|
|
206
|
+
|
|
207
|
+
// `const opts = { ssr: false }` option objects referenced by name.
|
|
208
|
+
const optionConsts = new Map<string, string>();
|
|
209
|
+
const constPattern = /const\s+([A-Za-z_$][\w$]*)\s*=\s*({[\s\S]*?})\s*(?:as\s+const\s*)?;/g;
|
|
210
|
+
let constMatch: RegExpExecArray | null;
|
|
211
|
+
while ((constMatch = constPattern.exec(source))) {
|
|
212
|
+
if (constMatch[1] && constMatch[2]) optionConsts.set(constMatch[1], constMatch[2]);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
for (const call of dynamicCallsFromSource(source, dynamicNames)) {
|
|
216
|
+
const target = dynamicImportTarget.exec(call.source);
|
|
217
|
+
if (!target?.[2]) continue;
|
|
218
|
+
// Token scan over the call plus any named option consts it references —
|
|
219
|
+
// tolerant of the compile-appended target argument (rewriteDynamicCallTargets).
|
|
220
|
+
const args = source.slice(call.open + 1, call.close);
|
|
221
|
+
const tail = args.slice(args.indexOf(target[0]) + target[0].length);
|
|
222
|
+
let options = tail;
|
|
223
|
+
for (const identifier of tail.match(/[A-Za-z_$][\w$]*/g) ?? []) {
|
|
224
|
+
const named = optionConsts.get(identifier);
|
|
225
|
+
if (named) options += `\n${named}`;
|
|
226
|
+
}
|
|
227
|
+
const ssrFalse = /\bssr\s*:\s*false\b/.test(options);
|
|
228
|
+
const visible =
|
|
229
|
+
/\bload\s*:\s*['"]visible['"]/.test(options) && !/\bssr\s*:\s*true\b/.test(options);
|
|
230
|
+
if (ssrFalse || visible) deferred.add(target[2]);
|
|
231
|
+
}
|
|
232
|
+
return deferred;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function topLevelArgCount(args: string) {
|
|
236
|
+
let count = 0;
|
|
237
|
+
let depth = 0;
|
|
238
|
+
let quote: string | null = null;
|
|
239
|
+
let sawArg = false;
|
|
240
|
+
|
|
241
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
242
|
+
const char = args[index];
|
|
243
|
+
if (quote) {
|
|
244
|
+
if (char === quote && args[index - 1] !== '\\') quote = null;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
248
|
+
quote = char;
|
|
249
|
+
sawArg = true;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (char === '(' || char === '[' || char === '{') depth += 1;
|
|
253
|
+
if (char === ')' || char === ']' || char === '}') depth -= 1;
|
|
254
|
+
if (char === ',' && depth === 0) {
|
|
255
|
+
if (sawArg) count += 1;
|
|
256
|
+
sawArg = false;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (!/\s/.test(char ?? '')) sawArg = true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return sawArg ? count + 1 : count;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function matchingParen(source: string, open: number) {
|
|
266
|
+
let depth = 0;
|
|
267
|
+
let quote: string | null = null;
|
|
268
|
+
|
|
269
|
+
for (let index = open; index < source.length; index += 1) {
|
|
270
|
+
const char = source[index];
|
|
271
|
+
const previous = source[index - 1];
|
|
272
|
+
|
|
273
|
+
if (quote) {
|
|
274
|
+
if (char === quote && previous !== '\\') quote = null;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
279
|
+
quote = char;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (char === '(') depth += 1;
|
|
284
|
+
if (char === ')') {
|
|
285
|
+
depth -= 1;
|
|
286
|
+
if (depth === 0) return index;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return -1;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function nextDynamicCallIndex(source: string, dynamicNames: Set<string>, fromIndex: number) {
|
|
294
|
+
let next = -1;
|
|
295
|
+
for (const name of dynamicNames) {
|
|
296
|
+
const index = source.indexOf(name, fromIndex);
|
|
297
|
+
if (index !== -1 && (next === -1 || index < next)) next = index;
|
|
298
|
+
}
|
|
299
|
+
return next;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function dynamicNameAt(source: string, dynamicNames: Set<string>, index: number) {
|
|
303
|
+
for (const name of dynamicNames) {
|
|
304
|
+
if (source.startsWith(name, index)) return name;
|
|
305
|
+
}
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// Tree-shaking for destructured dynamic imports (mirrors webpack's `import()` export usage analysis).
|
|
2
|
+
//
|
|
3
|
+
// `const { used } = await import('./mod')` only ever touches `used`, so the unused exports of './mod' are
|
|
4
|
+
// dead. esbuild cannot see this across the dynamic-import boundary (each module is compiled standalone and
|
|
5
|
+
// keeps every export), so the server module compiler rewrites the qualifying `import()` to a per-usage
|
|
6
|
+
// facade module that re-exports ONLY the destructured names, letting esbuild drop the rest when it inlines
|
|
7
|
+
// the target.
|
|
8
|
+
//
|
|
9
|
+
// Only the syntactic forms webpack tree-shakes are recognized:
|
|
10
|
+
// - `const|let|var { a, b: c } = await import('lit')` (named / renamed)
|
|
11
|
+
// - `const { nested: { inner } } = await import('lit')` (top-level key kept)
|
|
12
|
+
// - `const { default: d } = await import('lit')` (default kept)
|
|
13
|
+
// - `const {} = await import('lit')` (all exports dropped; side effects kept)
|
|
14
|
+
// - `/* webpackExports: [...] */` magic comment overrides the used set.
|
|
15
|
+
//
|
|
16
|
+
// A rest element, member access, re-assignment and `.then(({ ... }) => ...)` destructuring are NOT
|
|
17
|
+
// tree-shaken by webpack, so they never match here and the target keeps all exports.
|
|
18
|
+
|
|
19
|
+
export interface ShakeableDynamicImport {
|
|
20
|
+
/** Absolute index (inclusive) of the specifier string literal, quote included. */
|
|
21
|
+
literalStart: number;
|
|
22
|
+
/** Absolute index (exclusive) just past the closing quote. */
|
|
23
|
+
literalEnd: number;
|
|
24
|
+
/** The import specifier (without quotes). */
|
|
25
|
+
specifier: string;
|
|
26
|
+
/**
|
|
27
|
+
* Export names the destructuring keeps. An empty array means the module is
|
|
28
|
+
* imported purely for side effects (`const {} = ...`): drop every export.
|
|
29
|
+
*/
|
|
30
|
+
usedExports: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const declStart = /\b(?:const|let|var)\s+\{/g;
|
|
34
|
+
|
|
35
|
+
export function findShakeableDynamicImports(source: string): ShakeableDynamicImport[] {
|
|
36
|
+
const results: ShakeableDynamicImport[] = [];
|
|
37
|
+
let match: RegExpExecArray | null;
|
|
38
|
+
|
|
39
|
+
while ((match = declStart.exec(source))) {
|
|
40
|
+
const braceOpen = match.index + match[0].length - 1;
|
|
41
|
+
const braceClose = matchDelimiter(source, braceOpen, '{', '}');
|
|
42
|
+
if (braceClose < 0) continue;
|
|
43
|
+
|
|
44
|
+
let index = skipTrivia(source, braceClose + 1);
|
|
45
|
+
if (source[index] !== '=') continue;
|
|
46
|
+
index = skipTrivia(source, index + 1);
|
|
47
|
+
if (!matchesWord(source, index, 'await')) continue;
|
|
48
|
+
index = skipTrivia(source, index + 'await'.length);
|
|
49
|
+
if (!matchesWord(source, index, 'import')) continue;
|
|
50
|
+
index = skipTrivia(source, index + 'import'.length);
|
|
51
|
+
if (source[index] !== '(') continue;
|
|
52
|
+
|
|
53
|
+
const parenClose = matchDelimiter(source, index, '(', ')');
|
|
54
|
+
if (parenClose < 0) continue;
|
|
55
|
+
const argStart = index + 1;
|
|
56
|
+
const argText = source.slice(argStart, parenClose);
|
|
57
|
+
|
|
58
|
+
const literal = firstStringLiteral(argText);
|
|
59
|
+
if (!literal || hasTrailingArgument(argText, literal.end)) continue;
|
|
60
|
+
|
|
61
|
+
const patternExports = usedExportsFromPattern(source.slice(braceOpen, braceClose + 1));
|
|
62
|
+
if (patternExports === null) continue; // rest element — not shakeable
|
|
63
|
+
|
|
64
|
+
const webpackExports = webpackExportsFromArg(argText);
|
|
65
|
+
results.push({
|
|
66
|
+
literalStart: argStart + literal.start,
|
|
67
|
+
literalEnd: argStart + literal.end,
|
|
68
|
+
specifier: literal.value,
|
|
69
|
+
usedExports: webpackExports ?? patternExports,
|
|
70
|
+
});
|
|
71
|
+
declStart.lastIndex = parenClose + 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return results;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Parse an object destructuring pattern (braces included) into the top-level
|
|
78
|
+
// export names it binds. Returns null when a rest element is present (webpack
|
|
79
|
+
// gives up on tree-shaking then).
|
|
80
|
+
function usedExportsFromPattern(pattern: string): string[] | null {
|
|
81
|
+
const inner = pattern.slice(1, -1);
|
|
82
|
+
const names: string[] = [];
|
|
83
|
+
|
|
84
|
+
for (const rawPart of splitTopLevel(inner)) {
|
|
85
|
+
const part = rawPart.trim();
|
|
86
|
+
if (!part) continue;
|
|
87
|
+
if (part.startsWith('...')) return null;
|
|
88
|
+
|
|
89
|
+
const colon = topLevelColonIndex(part);
|
|
90
|
+
const keyRegion = colon >= 0 ? part.slice(0, colon) : part;
|
|
91
|
+
// `{ a = fallback }` shorthand with a default — the key is before `=`.
|
|
92
|
+
const eq = colon >= 0 ? -1 : keyRegion.indexOf('=');
|
|
93
|
+
const name = (eq >= 0 ? keyRegion.slice(0, eq) : keyRegion).trim();
|
|
94
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(name)) return null; // computed/string key — bail
|
|
95
|
+
names.push(name);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return names;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function webpackExportsFromArg(argText: string): string[] | undefined {
|
|
102
|
+
const match = /webpackExports\s*:\s*\[([^\]]*)\]/.exec(argText);
|
|
103
|
+
if (!match) return undefined;
|
|
104
|
+
const names: string[] = [];
|
|
105
|
+
const namePattern = /['"]([^'"]+)['"]/g;
|
|
106
|
+
let entry: RegExpExecArray | null;
|
|
107
|
+
while ((entry = namePattern.exec(match[1] ?? ''))) {
|
|
108
|
+
if (entry[1]) names.push(entry[1]);
|
|
109
|
+
}
|
|
110
|
+
return names;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Split a comma-separated list at top level, ignoring commas nested in braces,
|
|
114
|
+
// brackets, parens, strings, or comments.
|
|
115
|
+
function splitTopLevel(source: string): string[] {
|
|
116
|
+
const parts: string[] = [];
|
|
117
|
+
let depth = 0;
|
|
118
|
+
let start = 0;
|
|
119
|
+
let index = 0;
|
|
120
|
+
|
|
121
|
+
while (index < source.length) {
|
|
122
|
+
const skipped = skipStringOrComment(source, index);
|
|
123
|
+
if (skipped > index) {
|
|
124
|
+
index = skipped;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const char = source[index];
|
|
128
|
+
if (char === '{' || char === '[' || char === '(') depth += 1;
|
|
129
|
+
else if (char === '}' || char === ']' || char === ')') depth -= 1;
|
|
130
|
+
else if (char === ',' && depth === 0) {
|
|
131
|
+
parts.push(source.slice(start, index));
|
|
132
|
+
start = index + 1;
|
|
133
|
+
}
|
|
134
|
+
index += 1;
|
|
135
|
+
}
|
|
136
|
+
parts.push(source.slice(start));
|
|
137
|
+
return parts;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function topLevelColonIndex(source: string): number {
|
|
141
|
+
let depth = 0;
|
|
142
|
+
let index = 0;
|
|
143
|
+
while (index < source.length) {
|
|
144
|
+
const skipped = skipStringOrComment(source, index);
|
|
145
|
+
if (skipped > index) {
|
|
146
|
+
index = skipped;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const char = source[index];
|
|
150
|
+
if (char === '{' || char === '[' || char === '(') depth += 1;
|
|
151
|
+
else if (char === '}' || char === ']' || char === ')') depth -= 1;
|
|
152
|
+
else if (char === ':' && depth === 0) return index;
|
|
153
|
+
index += 1;
|
|
154
|
+
}
|
|
155
|
+
return -1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Index of the matching close delimiter for the open delimiter at `open`,
|
|
159
|
+
// respecting nested delimiters, strings, and comments. Returns -1 if unbalanced.
|
|
160
|
+
function matchDelimiter(source: string, open: number, openChar: string, closeChar: string): number {
|
|
161
|
+
let depth = 0;
|
|
162
|
+
let index = open;
|
|
163
|
+
while (index < source.length) {
|
|
164
|
+
const skipped = skipStringOrComment(source, index);
|
|
165
|
+
if (skipped > index) {
|
|
166
|
+
index = skipped;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const char = source[index];
|
|
170
|
+
if (char === openChar) depth += 1;
|
|
171
|
+
else if (char === closeChar) {
|
|
172
|
+
depth -= 1;
|
|
173
|
+
if (depth === 0) return index;
|
|
174
|
+
}
|
|
175
|
+
index += 1;
|
|
176
|
+
}
|
|
177
|
+
return -1;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Locate the first string literal in `text`, skipping leading whitespace and
|
|
181
|
+
// comments. Returns its bounds (quotes included, `end` exclusive) and value.
|
|
182
|
+
function firstStringLiteral(text: string): { start: number; end: number; value: string } | undefined {
|
|
183
|
+
let index = skipTrivia(text, 0);
|
|
184
|
+
const quote = text[index];
|
|
185
|
+
if (quote !== '"' && quote !== "'" && quote !== '`') return undefined;
|
|
186
|
+
const start = index;
|
|
187
|
+
index += 1;
|
|
188
|
+
let value = '';
|
|
189
|
+
while (index < text.length) {
|
|
190
|
+
const char = text[index];
|
|
191
|
+
if (char === '\\') {
|
|
192
|
+
value += text[index + 1] ?? '';
|
|
193
|
+
index += 2;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (char === quote) return { start, end: index + 1, value };
|
|
197
|
+
value += char;
|
|
198
|
+
index += 1;
|
|
199
|
+
}
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// True when a second top-level argument follows the specifier — `import('x', y)`
|
|
204
|
+
// is not a bare specifier import and is left untouched.
|
|
205
|
+
function hasTrailingArgument(argText: string, afterLiteral: number): boolean {
|
|
206
|
+
const index = skipTrivia(argText, afterLiteral);
|
|
207
|
+
return index < argText.length && argText[index] === ',' && Boolean(argText.slice(index + 1).trim());
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function matchesWord(source: string, index: number, word: string): boolean {
|
|
211
|
+
if (!source.startsWith(word, index)) return false;
|
|
212
|
+
const after = source[index + word.length];
|
|
213
|
+
return after === undefined || !/[\w$]/.test(after);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Skip whitespace and `//`/`/* */` comments starting at `index`.
|
|
217
|
+
function skipTrivia(source: string, index: number): number {
|
|
218
|
+
let current = index;
|
|
219
|
+
for (;;) {
|
|
220
|
+
while (current < source.length && /\s/.test(source[current] ?? '')) current += 1;
|
|
221
|
+
const skipped = skipComment(source, current);
|
|
222
|
+
if (skipped === current) return current;
|
|
223
|
+
current = skipped;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// If `index` starts a string or comment, return the index just past it;
|
|
228
|
+
// otherwise return `index` unchanged.
|
|
229
|
+
function skipStringOrComment(source: string, index: number): number {
|
|
230
|
+
const comment = skipComment(source, index);
|
|
231
|
+
if (comment > index) return comment;
|
|
232
|
+
const char = source[index];
|
|
233
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
234
|
+
let current = index + 1;
|
|
235
|
+
while (current < source.length) {
|
|
236
|
+
if (source[current] === '\\') {
|
|
237
|
+
current += 2;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (source[current] === char) return current + 1;
|
|
241
|
+
current += 1;
|
|
242
|
+
}
|
|
243
|
+
return source.length;
|
|
244
|
+
}
|
|
245
|
+
return index;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function skipComment(source: string, index: number): number {
|
|
249
|
+
if (source[index] === '/' && source[index + 1] === '/') {
|
|
250
|
+
let current = index + 2;
|
|
251
|
+
while (current < source.length && source[current] !== '\n') current += 1;
|
|
252
|
+
return current;
|
|
253
|
+
}
|
|
254
|
+
if (source[index] === '/' && source[index + 1] === '*') {
|
|
255
|
+
let current = index + 2;
|
|
256
|
+
while (current < source.length && !(source[current] === '*' && source[current + 1] === '/')) {
|
|
257
|
+
current += 1;
|
|
258
|
+
}
|
|
259
|
+
return Math.min(source.length, current + 2);
|
|
260
|
+
}
|
|
261
|
+
return index;
|
|
262
|
+
}
|
package/src/env.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parse } from 'dotenv';
|
|
4
|
+
|
|
5
|
+
type EnvMap = Record<string, string>;
|
|
6
|
+
|
|
7
|
+
export interface LoadedEnvFile {
|
|
8
|
+
env: EnvMap;
|
|
9
|
+
path: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface LoadEnvResult {
|
|
13
|
+
loadedEnvFiles: LoadedEnvFile[];
|
|
14
|
+
mode: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function loadEnv(root: string, options: { dev?: boolean } = {}): Promise<LoadEnvResult> {
|
|
18
|
+
const nodeEnvKey = 'NODE_ENV';
|
|
19
|
+
const env = process.env as Record<string, string | undefined>;
|
|
20
|
+
if (!env[nodeEnvKey]) env[nodeEnvKey] = options.dev ? 'development' : 'production';
|
|
21
|
+
|
|
22
|
+
const mode = process.env[nodeEnvKey] ?? 'production';
|
|
23
|
+
const loadedEnvFiles: LoadedEnvFile[] = [];
|
|
24
|
+
for (const file of envFilesForMode(mode)) {
|
|
25
|
+
const filePath = path.join(root, file);
|
|
26
|
+
if (!(await isEnvFile(filePath))) continue;
|
|
27
|
+
|
|
28
|
+
const parsed = parse(await readFile(filePath, 'utf8'));
|
|
29
|
+
const env = expandEnv(parsed);
|
|
30
|
+
for (const [key, value] of Object.entries(env)) {
|
|
31
|
+
if (process.env[key] === undefined) process.env[key] = value;
|
|
32
|
+
}
|
|
33
|
+
loadedEnvFiles.push({ path: file, env });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { mode, loadedEnvFiles };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function envFilesForMode(mode: string) {
|
|
40
|
+
return [
|
|
41
|
+
`.env.${mode}.local`,
|
|
42
|
+
mode === 'test' ? undefined : '.env.local',
|
|
43
|
+
`.env.${mode}`,
|
|
44
|
+
'.env',
|
|
45
|
+
].filter((file): file is string => Boolean(file));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function isEnvFile(file: string) {
|
|
49
|
+
try {
|
|
50
|
+
const info = await stat(file);
|
|
51
|
+
return info.isFile();
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false;
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function expandEnv(parsed: EnvMap) {
|
|
59
|
+
const env: EnvMap = {};
|
|
60
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
61
|
+
env[key] = expandValue(value, { ...parsed, ...env });
|
|
62
|
+
}
|
|
63
|
+
return env;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function expandValue(value: string, parsed: EnvMap): string {
|
|
67
|
+
let result = value;
|
|
68
|
+
for (
|
|
69
|
+
let index = searchLastEnvReference(result), attempts = 0;
|
|
70
|
+
index !== -1 && attempts < 100;
|
|
71
|
+
index = searchLastEnvReference(result), attempts += 1
|
|
72
|
+
) {
|
|
73
|
+
const match = result.slice(index).match(/^\$({?)([\w]+)(?::-([^}]*))?}?/);
|
|
74
|
+
if (!match) break;
|
|
75
|
+
|
|
76
|
+
const token = match[0] ?? '';
|
|
77
|
+
const braced = match[1] ?? '';
|
|
78
|
+
const key = match[2] ?? '';
|
|
79
|
+
const fallback = match[3] ?? '';
|
|
80
|
+
const hasClosingBrace = braced ? token.endsWith('}') : true;
|
|
81
|
+
if (!hasClosingBrace) break;
|
|
82
|
+
|
|
83
|
+
const replacement = process.env[key] || fallback || parsed[key] || '';
|
|
84
|
+
result = `${result.slice(0, index)}${replacement}${result.slice(index + token.length)}`;
|
|
85
|
+
}
|
|
86
|
+
return result.replace(/\\\$/g, '$');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function searchLastEnvReference(value: string) {
|
|
90
|
+
const matches = [...value.matchAll(/(?<!\\)\$/g)];
|
|
91
|
+
return matches.length > 0 ? (matches.at(-1)?.index ?? -1) : -1;
|
|
92
|
+
}
|