@vercel/ruby 2.4.0-canary.20260211174907.cdd2da6 → 2.4.0

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/package.json CHANGED
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "name": "@vercel/ruby",
3
3
  "author": "Nathan Cahill <nathan@nathancahill.com>",
4
- "version": "2.4.0-canary.20260211174907.cdd2da6",
4
+ "version": "2.4.0",
5
5
  "license": "Apache-2.0",
6
6
  "main": "./dist/index",
7
7
  "homepage": "https://vercel.com/docs/runtimes#official-runtimes/ruby",
8
8
  "files": [
9
9
  "dist",
10
10
  "vc_init.rb",
11
- "vc_init_dev.rb"
11
+ "vc_init_dev.rb",
12
+ "vc_utils.rb"
12
13
  ],
13
14
  "repository": {
14
15
  "type": "git",
@@ -21,15 +22,17 @@
21
22
  "@types/which": "3.0.0",
22
23
  "execa": "2.0.4",
23
24
  "fs-extra": "^7.0.1",
24
- "jest-junit": "16.0.0",
25
25
  "semver": "6.3.1",
26
+ "vitest": "2.0.3",
26
27
  "which": "3.0.0",
27
- "@vercel/build-utils": "13.4.0-canary.20260211174907.cdd2da6"
28
+ "@vercel/build-utils": "13.26.2"
28
29
  },
29
30
  "scripts": {
30
31
  "build": "node ../../utils/build-builder.mjs",
31
- "test": "jest --reporters=default --reporters=jest-junit --env node --verbose --runInBand --bail",
32
+ "test": "vitest run --config ../../vitest.config.mts",
32
33
  "test-e2e": "pnpm test",
33
- "type-check": "tsc --noEmit"
34
+ "type-check": "tsc --noEmit",
35
+ "vitest-run": "vitest -c ../../vitest.config.mts",
36
+ "vitest-e2e": "glob --absolute 'test/**/*.test.js' 'test/**/*.test.ts' 'tests/**/*.test.js' 'tests/**/*.test.ts'"
34
37
  }
35
38
  }
package/vc_init.rb CHANGED
@@ -3,6 +3,7 @@ require 'webrick'
3
3
  require 'net/http'
4
4
  require 'base64'
5
5
  require 'json'
6
+ require_relative 'vc__utils__ruby'
6
7
 
7
8
  $entrypoint = '__VC_HANDLER_FILENAME'
8
9
 
@@ -10,13 +11,17 @@ ENV['RAILS_ENV'] ||= 'production'
10
11
  ENV['RACK_ENV'] ||= 'production'
11
12
  ENV['RAILS_LOG_TO_STDOUT'] ||= '1'
12
13
 
13
- def rack_handler(httpMethod, path, body, headers)
14
+
15
+ $service_route_prefix = resolve_service_route_prefix
16
+
17
+ def rack_handler(httpMethod, path, body, headers, script_name = '')
14
18
  require 'rack'
15
19
 
16
20
  app, _ = Rack::Builder.parse_file($entrypoint)
17
21
  server = Rack::MockRequest.new app
18
22
 
19
23
  env = headers.transform_keys { |k| k.split('-').join('_').prepend('HTTP_').upcase }
24
+ env['SCRIPT_NAME'] = script_name unless script_name.empty?
20
25
  res = server.request(httpMethod, path, env.merge({ :input => body }))
21
26
 
22
27
  {
@@ -96,8 +101,13 @@ def vc__handler(event:, context:)
96
101
  body = Base64.decode64(body)
97
102
  end
98
103
 
104
+ path, script_name = apply_service_route_prefix_to_target(
105
+ path,
106
+ $service_route_prefix
107
+ )
108
+
99
109
  if $entrypoint.end_with? '.ru'
100
- return rack_handler(httpMethod, path, body, headers)
110
+ return rack_handler(httpMethod, path, body, headers, script_name)
101
111
  end
102
112
 
103
113
  return webrick_handler(httpMethod, path, body, headers)
package/vc_init_dev.rb CHANGED
@@ -10,12 +10,14 @@ require 'rack'
10
10
  require 'rack/handler/webrick'
11
11
  require 'webrick'
12
12
  require 'socket'
13
+ require_relative 'vc__utils__ruby'
13
14
 
14
15
  $stdout.sync = true
15
16
  $stderr.sync = true
16
17
 
17
18
  USER_ENTRYPOINT = "__VC_DEV_ENTRYPOINT__"
18
19
  PUBLIC_DIR = 'public'
20
+ SERVICE_ROUTE_PREFIX = resolve_service_route_prefix
19
21
 
20
22
  def build_user_app
21
23
  if USER_ENTRYPOINT.end_with?('.ru')
@@ -36,17 +38,33 @@ class StaticThenApp
36
38
  end
37
39
 
38
40
  def call(env)
39
- req_path = env['PATH_INFO'] || '/'
41
+ effective_env = env.dup
42
+ req_path, matched_prefix = strip_service_route_prefix(
43
+ effective_env['PATH_INFO'] || '/',
44
+ SERVICE_ROUTE_PREFIX
45
+ )
46
+ effective_env['PATH_INFO'] = req_path
47
+
48
+ unless matched_prefix.empty?
49
+ script_name = effective_env['SCRIPT_NAME'] || ''
50
+ if !script_name.empty? && script_name != '/'
51
+ script_name = script_name.sub(%r{/+\z}, '')
52
+ else
53
+ script_name = ''
54
+ end
55
+ effective_env['SCRIPT_NAME'] = "#{script_name}#{matched_prefix}"
56
+ end
57
+
40
58
  # Normalize path and guard against traversal
41
59
  safe = req_path.sub(/^\//, '')
42
60
  full = File.expand_path(safe, @base)
43
61
 
44
62
  if full.start_with?(@base + File::SEPARATOR) && File.file?(full)
45
63
  # Delegate to Rack::File which handles HEAD/GET correctly
46
- return @file_server.call(env)
64
+ return @file_server.call(effective_env)
47
65
  end
48
66
 
49
- @app.call(env)
67
+ @app.call(effective_env)
50
68
  end
51
69
  end
52
70
 
package/vc_utils.rb ADDED
@@ -0,0 +1,101 @@
1
+ require 'uri'
2
+
3
+ # Normalize a configured service prefix into canonical mount-path form.
4
+ #
5
+ # Returns an empty string when unset/blank/root ("/"), otherwise:
6
+ # - always starts with "/"
7
+ # - has no trailing slash
8
+ def normalize_service_route_prefix(raw_prefix)
9
+ return '' if raw_prefix.nil?
10
+
11
+ prefix = raw_prefix.strip
12
+ return '' if prefix.empty?
13
+
14
+ prefix = "/#{prefix}" unless prefix.start_with?('/')
15
+
16
+ if prefix != '/'
17
+ prefix = prefix.sub(%r{/+\z}, '')
18
+ prefix = '/' if prefix.empty?
19
+ end
20
+
21
+ prefix == '/' ? '' : prefix
22
+ end
23
+
24
+ # Return whether service-prefix stripping is explicitly enabled.
25
+ #
26
+ # Stripping is gated behind a dedicated env var so manually configured
27
+ # route prefixes can be preserved by default.
28
+ def service_route_prefix_strip_enabled?
29
+ raw = ENV['VERCEL_SERVICE_ROUTE_PREFIX_STRIP']
30
+ return false if raw.nil? || raw.empty?
31
+
32
+ %w[1 true].include?(raw.downcase)
33
+ end
34
+
35
+ def resolve_service_route_prefix
36
+ return '' unless service_route_prefix_strip_enabled?
37
+
38
+ normalize_service_route_prefix(ENV['VERCEL_SERVICE_ROUTE_PREFIX'])
39
+ end
40
+
41
+ # Split an HTTP request-target into [path, query].
42
+ #
43
+ # Supports:
44
+ # - origin-form: "/a/b?x=1"
45
+ # - absolute-form: "https://example.com/a/b?x=1"
46
+ # - asterisk-form: "*"
47
+ def split_request_target(target)
48
+ return ['/', ''] if target.nil? || target.empty?
49
+
50
+ begin
51
+ parsed = URI.parse(target)
52
+ if parsed.scheme && parsed.host
53
+ path = parsed.path.nil? || parsed.path.empty? ? '/' : parsed.path
54
+ return [path, parsed.query.to_s]
55
+ end
56
+ rescue URI::InvalidURIError
57
+ end
58
+
59
+ return ['*', ''] if target == '*'
60
+
61
+ path, query = target.split('?', 2)
62
+ path = '/' if path.nil? || path.empty?
63
+ path = "/#{path}" unless path.start_with?('/')
64
+ [path, query.to_s]
65
+ end
66
+
67
+ # Strip the configured service route prefix from a request path.
68
+ #
69
+ # Returns:
70
+ # - stripped path passed to the user app
71
+ # - matched mount prefix (empty when no prefix matched)
72
+ def strip_service_route_prefix(path, prefix)
73
+ return [path, ''] if path == '*'
74
+
75
+ normalized_path = path.nil? || path.empty? ? '/' : path
76
+ normalized_path = "/#{normalized_path}" unless normalized_path.start_with?('/')
77
+
78
+ normalized_prefix = prefix.to_s
79
+ return [normalized_path, ''] if normalized_prefix.empty?
80
+
81
+ return ['/', normalized_prefix] if normalized_path == normalized_prefix
82
+
83
+ if normalized_path.start_with?("#{normalized_prefix}/")
84
+ stripped = normalized_path[normalized_prefix.length..]
85
+ return [stripped.nil? || stripped.empty? ? '/' : stripped, normalized_prefix]
86
+ end
87
+
88
+ [normalized_path, '']
89
+ end
90
+
91
+ # Apply service-prefix stripping to a full request-target.
92
+ #
93
+ # Returns:
94
+ # - updated request-target (path + optional query)
95
+ # - matched mount prefix for Rack SCRIPT_NAME (or "")
96
+ def apply_service_route_prefix_to_target(target, prefix)
97
+ path, query = split_request_target(target)
98
+ path, script_name = strip_service_route_prefix(path, prefix)
99
+ updated = query.empty? ? path : "#{path}?#{query}"
100
+ [updated, script_name]
101
+ end