@vercel/backends 0.0.45 → 0.0.47

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.
Files changed (2) hide show
  1. package/dist/index.mjs +226 -8
  2. package/package.json +2 -2
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ import { delimiter, dirname, extname, join } from "path";
3
3
  import { FileBlob, FileFsRef, NodejsLambda, Span, debug, defaultCachePathGlob, download, execCommand, getEnvForPackageManager, getLambdaOptionsFromFunction, getNodeBinPaths, getNodeVersion, glob, isBackendFramework, isBunVersion, isExperimentalBackendsWithoutIntrospectionEnabled, runNpmInstall, runPackageJsonScript, scanParentDirs } from "@vercel/build-utils";
4
4
  import { createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
5
5
  import { lstat, readFile, rm, stat } from "node:fs/promises";
6
- import { dirname as dirname$1, extname as extname$1, isAbsolute, join as join$1, relative } from "node:path";
6
+ import { basename, dirname as dirname$1, extname as extname$1, isAbsolute, join as join$1, relative } from "node:path";
7
7
  import { build as build$2 } from "rolldown";
8
8
  import { exports } from "resolve.exports";
9
9
  import { isNativeError } from "node:util/types";
@@ -673,6 +673,167 @@ const resolveEntrypointAndFormat = async (args) => {
673
673
  };
674
674
  };
675
675
 
676
+ //#endregion
677
+ //#region src/service-vc-init.ts
678
+ async function applyServiceVcInit(args) {
679
+ const { format, extension } = await resolveShimFormat(args);
680
+ const handlerDir = dirname$1(args.handler);
681
+ const vcInitName = `${basename(args.handler, extname$1(args.handler))}.__vc_service_vc_init${extension}`;
682
+ const vcInitHandler = handlerDir === "." ? vcInitName : join$1(handlerDir, vcInitName);
683
+ const handlerImportPath = `./${basename(args.handler)}`;
684
+ const vcInitSource = format === "esm" ? createEsmServiceVcInit(handlerImportPath) : createCjsServiceVcInit(handlerImportPath);
685
+ return {
686
+ handler: vcInitHandler,
687
+ files: {
688
+ ...args.files,
689
+ [vcInitHandler]: new FileBlob({
690
+ data: vcInitSource,
691
+ mode: 420
692
+ })
693
+ }
694
+ };
695
+ }
696
+ async function resolveShimFormat(args) {
697
+ const { format } = await resolveEntrypointAndFormat({
698
+ entrypoint: args.handler,
699
+ workPath: args.workPath
700
+ });
701
+ return {
702
+ format,
703
+ extension: extname$1(args.handler) || (format === "esm" ? ".mjs" : ".cjs")
704
+ };
705
+ }
706
+ const sharedShimPrelude = String.raw`
707
+ const PATCH_SYMBOL = Symbol.for('vc.service.route-prefix-strip.patch')
708
+
709
+ function normalizeServiceRoutePrefix(rawPrefix) {
710
+ if (!rawPrefix) {
711
+ return ''
712
+ }
713
+
714
+ let prefix = String(rawPrefix).trim()
715
+ if (!prefix) {
716
+ return ''
717
+ }
718
+
719
+ if (!prefix.startsWith('/')) {
720
+ prefix = '/' + prefix
721
+ }
722
+
723
+ if (prefix !== '/') {
724
+ prefix = prefix.replace(/\/+$/, '')
725
+ }
726
+
727
+ return prefix === '/' ? '' : prefix
728
+ }
729
+
730
+ function getServiceRoutePrefix() {
731
+ const enabled = String(
732
+ process.env.VERCEL_SERVICE_ROUTE_PREFIX_STRIP || ''
733
+ ).toLowerCase()
734
+ if (enabled !== '1' && enabled !== 'true') {
735
+ return ''
736
+ }
737
+
738
+ return normalizeServiceRoutePrefix(process.env.VERCEL_SERVICE_ROUTE_PREFIX || '')
739
+ }
740
+
741
+ function stripServiceRoutePrefix(requestUrl, prefix) {
742
+ if (typeof requestUrl !== 'string' || requestUrl === '*') {
743
+ return requestUrl
744
+ }
745
+
746
+ const queryIndex = requestUrl.indexOf('?')
747
+ const rawPath =
748
+ queryIndex === -1 ? requestUrl : requestUrl.slice(0, queryIndex)
749
+ const query = queryIndex === -1 ? '' : requestUrl.slice(queryIndex)
750
+
751
+ let path = rawPath || '/'
752
+ if (!path.startsWith('/')) {
753
+ path = '/' + path
754
+ }
755
+
756
+ if (!prefix) {
757
+ return path + query
758
+ }
759
+
760
+ if (path === prefix) {
761
+ return '/' + query
762
+ }
763
+
764
+ if (path.startsWith(prefix + '/')) {
765
+ return path.slice(prefix.length) + query
766
+ }
767
+
768
+ return path + query
769
+ }
770
+
771
+ function patchServerRequestUrl(ServerCtor) {
772
+ const prefix = getServiceRoutePrefix()
773
+ if (!prefix || globalThis[PATCH_SYMBOL]) {
774
+ return
775
+ }
776
+
777
+ globalThis[PATCH_SYMBOL] = true
778
+
779
+ const originalEmit = ServerCtor.prototype.emit
780
+ ServerCtor.prototype.emit = function patchedEmit(event, request, ...args) {
781
+ if (event === 'request' && request && typeof request.url === 'string') {
782
+ request.url = stripServiceRoutePrefix(request.url, prefix)
783
+ }
784
+
785
+ return originalEmit.call(this, event, request, ...args)
786
+ }
787
+ }
788
+ `;
789
+ function createEsmServiceVcInit(handlerImportPath) {
790
+ return `
791
+ import { Server } from 'node:http'
792
+
793
+ ${sharedShimPrelude}
794
+
795
+ // Patch the HTTP server before loading user code so apps that attach request
796
+ // listeners during module evaluation see the stripped service-relative URL.
797
+ patchServerRequestUrl(Server)
798
+
799
+ const originalModule = await import(${JSON.stringify(handlerImportPath)})
800
+
801
+ /**
802
+ * Match the Node serverless loader behavior: TS/CJS/ESM interop can leave us
803
+ * with nested \`.default\` wrappers, so peel off a few layers to recover the
804
+ * actual user entrypoint shape.
805
+ */
806
+ function unwrapDefaultExport(value) {
807
+ let current = value
808
+ for (let i = 0; i < 5; i++) {
809
+ if (current && typeof current === 'object' && 'default' in current && current.default) {
810
+ current = current.default
811
+ } else {
812
+ break
813
+ }
814
+ }
815
+ return current
816
+ }
817
+
818
+ const entrypoint = unwrapDefaultExport(originalModule)
819
+
820
+ // Re-export the resolved entrypoint so the surrounding runtime still sees the
821
+ // same handler shape after this service bootstrap runs.
822
+ export default typeof entrypoint === 'undefined' ? originalModule : entrypoint
823
+ `.trimStart();
824
+ }
825
+ function createCjsServiceVcInit(handlerImportPath) {
826
+ return `
827
+ const { Server } = require('node:http')
828
+
829
+ ${sharedShimPrelude}
830
+
831
+ patchServerRequestUrl(Server)
832
+
833
+ module.exports = require(${JSON.stringify(handlerImportPath)})
834
+ `.trimStart();
835
+ }
836
+
676
837
  //#endregion
677
838
  //#region src/rolldown/nft.ts
678
839
  const nft = async (args) => {
@@ -1475,10 +1636,23 @@ const build = async (args) => {
1475
1636
  sourceFile: entrypoint,
1476
1637
  config: args.config
1477
1638
  });
1639
+ const serviceRoutePrefix = normalizeServiceRoutePrefix(args.config?.routePrefix ?? args.service?.routePrefix);
1640
+ const shouldStripServiceRoutePrefix = !!serviceRoutePrefix && (typeof args.config?.serviceName === "string" || !!args.service);
1641
+ let lambdaFiles = files;
1642
+ let lambdaHandler = handler;
1643
+ if (shouldStripServiceRoutePrefix) {
1644
+ const shimmedLambda = await applyServiceVcInit({
1645
+ files,
1646
+ handler,
1647
+ workPath: nftWorkPath
1648
+ });
1649
+ lambdaFiles = shimmedLambda.files;
1650
+ lambdaHandler = shimmedLambda.handler;
1651
+ }
1478
1652
  const lambda = new NodejsLambda({
1479
1653
  runtime: nodeVersion.runtime,
1480
- handler,
1481
- files,
1654
+ handler: lambdaHandler,
1655
+ files: lambdaFiles,
1482
1656
  framework: rolldownResult.framework,
1483
1657
  shouldAddHelpers: false,
1484
1658
  shouldAddSourcemapSupport: true,
@@ -1486,18 +1660,35 @@ const build = async (args) => {
1486
1660
  ...functionConfigOverrides,
1487
1661
  shouldDisableAutomaticFetchInstrumentation: process.env.VERCEL_TRACING_DISABLE_AUTOMATIC_FETCH_INSTRUMENTATION === "1"
1488
1662
  });
1663
+ if (shouldStripServiceRoutePrefix && serviceRoutePrefix) lambda.environment = {
1664
+ ...lambda.environment,
1665
+ VERCEL_SERVICE_ROUTE_PREFIX: serviceRoutePrefix,
1666
+ VERCEL_SERVICE_ROUTE_PREFIX_STRIP: "1"
1667
+ };
1668
+ const serviceName = typeof args.config?.serviceName === "string" && args.config.serviceName !== "" ? args.config.serviceName : void 0;
1669
+ const internalServiceFunctionPath = typeof serviceName === "string" && serviceName !== "" ? `/_svc/${serviceName}/index` : void 0;
1670
+ const internalServiceOutputPath = internalServiceFunctionPath?.slice(1);
1671
+ const remapRouteDestination = (route) => {
1672
+ const prefixedRoute = maybePrefixServiceRouteSource(route, serviceRoutePrefix);
1673
+ if (!internalServiceFunctionPath || !route.dest) return prefixedRoute;
1674
+ return {
1675
+ ...prefixedRoute,
1676
+ dest: internalServiceFunctionPath
1677
+ };
1678
+ };
1489
1679
  const routes = [
1490
1680
  { handle: "filesystem" },
1491
- ...introspectionResult.routes,
1681
+ ...introspectionResult.routes.map(remapRouteDestination),
1492
1682
  {
1493
- src: "/(.*)",
1494
- dest: "/"
1683
+ src: getServiceCatchallSource(serviceRoutePrefix),
1684
+ dest: internalServiceFunctionPath ?? "/"
1495
1685
  }
1496
1686
  ];
1497
- const output = { index: lambda };
1687
+ const output = internalServiceOutputPath ? { [internalServiceOutputPath]: lambda } : { index: lambda };
1498
1688
  for (const route of routes) if (route.dest) {
1499
1689
  if (route.dest === "/") continue;
1500
- output[route.dest] = lambda;
1690
+ const outputPath = route.dest === internalServiceFunctionPath && internalServiceOutputPath ? internalServiceOutputPath : route.dest;
1691
+ output[outputPath] = lambda;
1501
1692
  }
1502
1693
  return {
1503
1694
  routes,
@@ -1509,6 +1700,33 @@ const prepareCache = ({ repoRootPath, workPath }) => {
1509
1700
  return glob(defaultCachePathGlob, repoRootPath || workPath);
1510
1701
  };
1511
1702
  const normalizeArray = (value) => Array.isArray(value) ? value : value ? [value] : [];
1703
+ const normalizeServiceRoutePrefix = (routePrefix) => {
1704
+ if (typeof routePrefix !== "string" || routePrefix === "" || routePrefix === ".") return;
1705
+ let normalized = routePrefix.startsWith("/") ? routePrefix : `/${routePrefix}`;
1706
+ if (normalized !== "/" && normalized.endsWith("/")) normalized = normalized.slice(0, -1);
1707
+ return normalized === "/" ? void 0 : normalized;
1708
+ };
1709
+ const maybePrefixServiceRouteSource = (route, routePrefix) => {
1710
+ if (!routePrefix || typeof route.dest !== "string" || !route.dest.startsWith("/")) return route;
1711
+ return {
1712
+ ...route,
1713
+ src: getPrefixedRouteSource(route.src, route.dest, routePrefix)
1714
+ };
1715
+ };
1716
+ const getPrefixedRouteSource = (routeSource, routePath, routePrefix) => {
1717
+ if (!routeSource) return routeSource;
1718
+ if (routePath === routePrefix || routePath.startsWith(`${routePrefix}/`)) return routeSource;
1719
+ const escapedRoutePrefix = toRegexSource(routePrefix);
1720
+ if (routeSource.startsWith("^(?:")) return `^(?:${escapedRoutePrefix}${routeSource.slice(4)}`;
1721
+ if (routeSource.startsWith("^")) return `^${escapedRoutePrefix}${routeSource.slice(1)}`;
1722
+ return `${escapedRoutePrefix}${routeSource}`;
1723
+ };
1724
+ const getServiceCatchallSource = (routePrefix) => {
1725
+ if (!routePrefix) return "/(.*)";
1726
+ return `^${escapeForRegex(routePrefix)}(?:/(.*))?$`;
1727
+ };
1728
+ const escapeForRegex = (value) => value.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
1729
+ const toRegexSource = (value) => escapeForRegex(value).replaceAll("/", "\\/");
1512
1730
 
1513
1731
  //#endregion
1514
1732
  export { build, build$1 as cervelBuild, serve as cervelServe, findEntrypoint, findEntrypointOrThrow, getBuildSummary, introspectApp, nodeFileTrace, prepareCache, srvxOptions, version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/backends",
3
- "version": "0.0.45",
3
+ "version": "0.0.47",
4
4
  "license": "Apache-2.0",
5
5
  "main": "./dist/index.mjs",
6
6
  "homepage": "https://vercel.com/docs",
@@ -34,7 +34,7 @@
34
34
  "srvx": "0.8.9",
35
35
  "tsx": "4.21.0",
36
36
  "zod": "3.22.4",
37
- "@vercel/build-utils": "13.8.0"
37
+ "@vercel/build-utils": "13.8.1"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "typescript": "^4.0.0 || ^5.0.0"