@vercel/ruby 2.4.0-canary.20260211174907.cdd2da6 → 2.5.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/dist/index.js CHANGED
@@ -2371,9 +2371,9 @@ var require_end_of_stream = __commonJS({
2371
2371
  }
2372
2372
  });
2373
2373
 
2374
- // ../../node_modules/.pnpm/pump@3.0.2/node_modules/pump/index.js
2374
+ // ../../node_modules/.pnpm/pump@3.0.4/node_modules/pump/index.js
2375
2375
  var require_pump = __commonJS({
2376
- "../../node_modules/.pnpm/pump@3.0.2/node_modules/pump/index.js"(exports, module2) {
2376
+ "../../node_modules/.pnpm/pump@3.0.4/node_modules/pump/index.js"(exports, module2) {
2377
2377
  var once = require_once();
2378
2378
  var eos = require_end_of_stream();
2379
2379
  var fs;
@@ -2383,7 +2383,7 @@ var require_pump = __commonJS({
2383
2383
  }
2384
2384
  var noop = function() {
2385
2385
  };
2386
- var ancient = /^v?\.0/.test(process.version);
2386
+ var ancient = typeof process === "undefined" ? false : /^v?\.0/.test(process.version);
2387
2387
  var isFn = function(fn) {
2388
2388
  return typeof fn === "function";
2389
2389
  };
@@ -6940,6 +6940,7 @@ var require_semver2 = __commonJS({
6940
6940
  var src_exports = {};
6941
6941
  __export(src_exports, {
6942
6942
  build: () => build,
6943
+ diagnostics: () => diagnostics,
6943
6944
  shouldServe: () => shouldServe,
6944
6945
  startDevServer: () => startDevServer,
6945
6946
  version: () => version
@@ -6949,7 +6950,7 @@ var import_os2 = require("os");
6949
6950
  var import_path3 = require("path");
6950
6951
  var import_execa2 = __toESM(require_execa());
6951
6952
  var import_fs_extra = __toESM(require_lib());
6952
- var import_build_utils3 = require("@vercel/build-utils");
6953
+ var import_build_utils4 = require("@vercel/build-utils");
6953
6954
 
6954
6955
  // src/install-ruby.ts
6955
6956
  var import_execa = __toESM(require_execa());
@@ -7156,12 +7157,16 @@ function isInstalled({ major, minor }) {
7156
7157
  return Boolean(import_which.default.sync((0, import_path.join)(gemHome, "bin/ruby"), { nothrow: true })) && Boolean(import_which.default.sync((0, import_path.join)(gemHome, "bin/gem"), { nothrow: true }));
7157
7158
  }
7158
7159
 
7160
+ // src/diagnostics.ts
7161
+ var import_build_utils2 = require("@vercel/build-utils");
7162
+ var diagnostics = (0, import_build_utils2.createDiagnostics)("ruby");
7163
+
7159
7164
  // src/start-dev-server.ts
7160
7165
  var import_child_process2 = require("child_process");
7161
7166
  var import_fs = require("fs");
7162
7167
  var import_path2 = require("path");
7163
7168
  var import_which2 = __toESM(require_lib2());
7164
- var import_build_utils2 = require("@vercel/build-utils");
7169
+ var import_build_utils3 = require("@vercel/build-utils");
7165
7170
  function silenceNodeWarnings() {
7166
7171
  const original = process.emitWarning.bind(
7167
7172
  process
@@ -7199,12 +7204,12 @@ function installGlobalCleanupHandlers() {
7199
7204
  try {
7200
7205
  process.kill(info.pid, "SIGTERM");
7201
7206
  } catch (err) {
7202
- (0, import_build_utils2.debug)(`Error sending SIGTERM to ${info.pid}: ${err}`);
7207
+ (0, import_build_utils3.debug)(`Error sending SIGTERM to ${info.pid}: ${err}`);
7203
7208
  }
7204
7209
  try {
7205
7210
  process.kill(info.pid, "SIGKILL");
7206
7211
  } catch (err) {
7207
- (0, import_build_utils2.debug)(`Error sending SIGKILL to ${info.pid}: ${err}`);
7212
+ (0, import_build_utils3.debug)(`Error sending SIGKILL to ${info.pid}: ${err}`);
7208
7213
  }
7209
7214
  PERSISTENT_SERVERS.delete(key);
7210
7215
  }
@@ -7212,7 +7217,7 @@ function installGlobalCleanupHandlers() {
7212
7217
  try {
7213
7218
  restoreWarnings();
7214
7219
  } catch (err) {
7215
- (0, import_build_utils2.debug)(`Error restoring warnings: ${err}`);
7220
+ (0, import_build_utils3.debug)(`Error restoring warnings: ${err}`);
7216
7221
  }
7217
7222
  restoreWarnings = null;
7218
7223
  }
@@ -7232,14 +7237,18 @@ function createDevRubyShim(workPath, entrypoint) {
7232
7237
  const vercelRubyDir = (0, import_path2.join)(workPath, ".vercel", "ruby");
7233
7238
  (0, import_fs.mkdirSync)(vercelRubyDir, { recursive: true });
7234
7239
  const shimPath = (0, import_path2.join)(vercelRubyDir, `vc_init_dev.rb`);
7240
+ const utilsPath = (0, import_path2.join)(vercelRubyDir, "vc__utils__ruby.rb");
7235
7241
  const templatePath = (0, import_path2.join)(__dirname, "..", "vc_init_dev.rb");
7242
+ const utilsTemplatePath = (0, import_path2.join)(__dirname, "..", "vc_utils.rb");
7236
7243
  const template = (0, import_fs.readFileSync)(templatePath, "utf8");
7244
+ const utilsTemplate = (0, import_fs.readFileSync)(utilsTemplatePath, "utf8");
7237
7245
  const shimSource = template.replace(/__VC_DEV_ENTRYPOINT__/g, entrypoint);
7238
7246
  (0, import_fs.writeFileSync)(shimPath, shimSource, "utf8");
7239
- (0, import_build_utils2.debug)(`Prepared Ruby dev shim at ${shimPath}`);
7247
+ (0, import_fs.writeFileSync)(utilsPath, utilsTemplate, "utf8");
7248
+ (0, import_build_utils3.debug)(`Prepared Ruby dev shim at ${shimPath}`);
7240
7249
  return shimPath;
7241
7250
  } catch (err) {
7242
- (0, import_build_utils2.debug)(`Failed to prepare Ruby dev shim: ${err?.message || err}`);
7251
+ (0, import_build_utils3.debug)(`Failed to prepare Ruby dev shim: ${err?.message || err}`);
7243
7252
  return null;
7244
7253
  }
7245
7254
  }
@@ -7260,8 +7269,22 @@ async function run(cmd, args, opts) {
7260
7269
  env: opts.env,
7261
7270
  stdio: ["ignore", "pipe", "pipe"]
7262
7271
  });
7263
- child.stdout?.on("data", (buf) => process.stdout.write(buf));
7264
- child.stderr?.on("data", (buf) => process.stderr.write(buf));
7272
+ child.stdout?.on("data", (data) => {
7273
+ const chunk = Buffer.isBuffer(data) ? data : Buffer.from(data);
7274
+ if (opts.onStdout) {
7275
+ opts.onStdout(chunk);
7276
+ } else {
7277
+ process.stdout.write(chunk.toString());
7278
+ }
7279
+ });
7280
+ child.stderr?.on("data", (data) => {
7281
+ const chunk = Buffer.isBuffer(data) ? data : Buffer.from(data);
7282
+ if (opts.onStderr) {
7283
+ opts.onStderr(chunk);
7284
+ } else {
7285
+ process.stderr.write(chunk.toString());
7286
+ }
7287
+ });
7265
7288
  child.on("close", (code, signal) => resolve({ code, signal }));
7266
7289
  });
7267
7290
  }
@@ -7309,7 +7332,7 @@ var startDevServer = async (opts) => {
7309
7332
  const shimPath = createDevRubyShim(workPath, entrypoint);
7310
7333
  if (!shimPath) {
7311
7334
  rejectChildReady(
7312
- new import_build_utils2.NowBuildError({
7335
+ new import_build_utils3.NowBuildError({
7313
7336
  code: "RUBY_DEV_SHIM_ERROR",
7314
7337
  message: "Failed to create Ruby dev shim."
7315
7338
  })
@@ -7325,11 +7348,16 @@ var startDevServer = async (opts) => {
7325
7348
  }
7326
7349
  const checkDeps = async () => {
7327
7350
  if (gemfile && bundlerPath) {
7328
- (0, import_build_utils2.debug)(`Running "bundle check" for ${gemfile}`);
7351
+ (0, import_build_utils3.debug)(`Running "bundle check" for ${gemfile}`);
7329
7352
  const check = await run(
7330
7353
  bundlerPath,
7331
7354
  ["check", "--gemfile", gemfile],
7332
- { cwd: projectDir, env }
7355
+ {
7356
+ cwd: projectDir,
7357
+ env,
7358
+ onStdout: opts.onStdout,
7359
+ onStderr: opts.onStderr
7360
+ }
7333
7361
  );
7334
7362
  if (check.code !== 0) {
7335
7363
  return false;
@@ -7339,7 +7367,7 @@ var startDevServer = async (opts) => {
7339
7367
  };
7340
7368
  checkDeps().then((ok) => {
7341
7369
  if (!ok) {
7342
- const err = new import_build_utils2.NowBuildError({
7370
+ const err = new import_build_utils3.NowBuildError({
7343
7371
  code: "RUBY_GEMS_MISSING",
7344
7372
  message: 'Required gems are not installed. Run "vercel build" and try again.'
7345
7373
  });
@@ -7352,25 +7380,25 @@ var startDevServer = async (opts) => {
7352
7380
  cmd = bundlerPath;
7353
7381
  args = ["exec", "ruby", shimPath];
7354
7382
  }
7355
- (0, import_build_utils2.debug)(`Starting Ruby dev server: ${cmd} ${args.join(" ")}`);
7383
+ (0, import_build_utils3.debug)(`Starting Ruby dev server: ${cmd} ${args.join(" ")}`);
7356
7384
  const child = (0, import_child_process2.spawn)(cmd, args, {
7357
7385
  cwd: workPath,
7358
7386
  env,
7359
- stdio: ["inherit", "pipe", "pipe"]
7387
+ stdio: ["ignore", "pipe", "pipe"]
7360
7388
  });
7361
7389
  childProcess = child;
7362
7390
  stdoutLogListener = (buf) => {
7363
- const s = buf.toString();
7364
- for (const line of s.split(/\r?\n/)) {
7365
- if (line)
7366
- process.stdout.write(line.endsWith("\n") ? line : line + "\n");
7391
+ if (opts.onStdout) {
7392
+ opts.onStdout(buf);
7393
+ } else {
7394
+ process.stdout.write(buf.toString());
7367
7395
  }
7368
7396
  };
7369
7397
  stderrLogListener = (buf) => {
7370
- const s = buf.toString();
7371
- for (const line of s.split(/\r?\n/)) {
7372
- if (line)
7373
- process.stderr.write(line.endsWith("\n") ? line : line + "\n");
7398
+ if (opts.onStderr) {
7399
+ opts.onStderr(buf);
7400
+ } else {
7401
+ process.stderr.write(buf.toString());
7374
7402
  }
7375
7403
  };
7376
7404
  child.stdout?.on("data", stdoutLogListener);
@@ -7448,7 +7476,7 @@ async function matchPaths(configPatterns, workPath) {
7448
7476
  }
7449
7477
  const patternPaths = await Promise.all(
7450
7478
  patterns.map(async (pattern) => {
7451
- const files = await (0, import_build_utils3.glob)(pattern, workPath);
7479
+ const files = await (0, import_build_utils4.glob)(pattern, workPath);
7452
7480
  return Object.keys(files);
7453
7481
  })
7454
7482
  );
@@ -7477,8 +7505,8 @@ async function prepareGemfile(gemfilePath, major) {
7477
7505
  return { modified };
7478
7506
  }
7479
7507
  async function bundleLock(bundlerPath, gemfilePath, rubyPath) {
7480
- const bundleAppConfig = await (0, import_build_utils3.getWriteableDirectory)();
7481
- const bundlerEnv = (0, import_build_utils3.cloneEnv)(process.env, {
7508
+ const bundleAppConfig = await (0, import_build_utils4.getWriteableDirectory)();
7509
+ const bundlerEnv = (0, import_build_utils4.cloneEnv)(process.env, {
7482
7510
  PATH: `${(0, import_path3.dirname)(rubyPath)}:${(0, import_path3.dirname)(bundlerPath)}:${process.env.PATH}`,
7483
7511
  BUNDLE_SILENCE_ROOT_WARNING: "1",
7484
7512
  BUNDLE_APP_CONFIG: bundleAppConfig,
@@ -7493,7 +7521,7 @@ async function bundleLock(bundlerPath, gemfilePath, rubyPath) {
7493
7521
  const darwinPlatform = darwinMajor ? `${darwinArchToken}-darwin-${darwinMajor}` : `${darwinArchToken}-darwin`;
7494
7522
  platforms.push(darwinPlatform);
7495
7523
  }
7496
- (0, import_build_utils3.debug)(`ruby: ensuring Gemfile.lock has platforms ${platforms.join(", ")}`);
7524
+ (0, import_build_utils4.debug)(`ruby: ensuring Gemfile.lock has platforms ${platforms.join(", ")}`);
7497
7525
  const lockArgs = ["lock"];
7498
7526
  for (const platform of platforms) {
7499
7527
  lockArgs.push("--add-platform", platform);
@@ -7511,8 +7539,8 @@ async function bundleLock(bundlerPath, gemfilePath, rubyPath) {
7511
7539
  }
7512
7540
  }
7513
7541
  async function bundleInstall(bundlePath, bundleDir, gemfilePath, rubyPath) {
7514
- const bundleAppConfig = await (0, import_build_utils3.getWriteableDirectory)();
7515
- const bundlerEnv = (0, import_build_utils3.cloneEnv)(process.env, {
7542
+ const bundleAppConfig = await (0, import_build_utils4.getWriteableDirectory)();
7543
+ const bundlerEnv = (0, import_build_utils4.cloneEnv)(process.env, {
7516
7544
  PATH: `${(0, import_path3.dirname)(rubyPath)}:${(0, import_path3.dirname)(bundlePath)}:${process.env.PATH}`,
7517
7545
  BUNDLE_SILENCE_ROOT_WARNING: "1",
7518
7546
  BUNDLE_APP_CONFIG: bundleAppConfig,
@@ -7521,7 +7549,7 @@ async function bundleInstall(bundlePath, bundleDir, gemfilePath, rubyPath) {
7521
7549
  BUNDLE_PATH: bundleDir,
7522
7550
  BUNDLE_FROZEN: "true"
7523
7551
  });
7524
- (0, import_build_utils3.debug)('running "bundle install"');
7552
+ (0, import_build_utils4.debug)('running "bundle install"');
7525
7553
  const installRes = await (0, import_execa2.default)(
7526
7554
  bundlePath,
7527
7555
  ["install", "--gemfile", gemfilePath],
@@ -7543,12 +7571,13 @@ var build = async ({
7543
7571
  files,
7544
7572
  entrypoint,
7545
7573
  config,
7546
- meta = {}
7574
+ meta = {},
7575
+ service
7547
7576
  }) => {
7548
- await (0, import_build_utils3.download)(files, workPath, meta);
7577
+ await (0, import_build_utils4.download)(files, workPath, meta);
7549
7578
  const entrypointFsDirname = (0, import_path3.join)(workPath, (0, import_path3.dirname)(entrypoint));
7550
7579
  const gemfileName = "Gemfile";
7551
- let gemfilePath = await (0, import_build_utils3.walkParentDirs)({
7580
+ let gemfilePath = await (0, import_build_utils4.walkParentDirs)({
7552
7581
  base: workPath,
7553
7582
  start: entrypointFsDirname,
7554
7583
  filename: gemfileName
@@ -7561,17 +7590,17 @@ var build = async ({
7561
7590
  const { gemHome, bundlerPath, vendorPath, runtime, rubyPath, major } = await installBundler(meta, gemfileContents);
7562
7591
  process.env.GEM_HOME = gemHome;
7563
7592
  try {
7564
- (0, import_build_utils3.debug)("preparing Gemfile");
7593
+ (0, import_build_utils4.debug)("preparing Gemfile");
7565
7594
  await prepareGemfile(gemfilePath, major);
7566
- (0, import_build_utils3.debug)('running "bundle lock"');
7595
+ (0, import_build_utils4.debug)('running "bundle lock"');
7567
7596
  await bundleLock(bundlerPath, gemfilePath, rubyPath);
7568
7597
  } catch (err) {
7569
- (0, import_build_utils3.debug)(
7598
+ (0, import_build_utils4.debug)(
7570
7599
  "failed to normalize Gemfile/lockfile before vendor check",
7571
7600
  err
7572
7601
  );
7573
7602
  }
7574
- (0, import_build_utils3.debug)(`Checking existing vendor directory at "${vendorPath}"`);
7603
+ (0, import_build_utils4.debug)(`Checking existing vendor directory at "${vendorPath}"`);
7575
7604
  const vendorDir = (0, import_path3.join)(workPath, vendorPath);
7576
7605
  const bundleDir = (0, import_path3.join)(workPath, "vendor", "bundle");
7577
7606
  const relativeVendorDir = (0, import_path3.join)(entrypointFsDirname, vendorPath);
@@ -7579,31 +7608,34 @@ var build = async ({
7579
7608
  const hasRelativeVendorDir = await (0, import_fs_extra.pathExists)(relativeVendorDir);
7580
7609
  if (hasRelativeVendorDir) {
7581
7610
  if (hasRootVendorDir) {
7582
- (0, import_build_utils3.debug)(
7611
+ (0, import_build_utils4.debug)(
7583
7612
  "found two vendor directories, choosing the vendor directory relative to entrypoint"
7584
7613
  );
7585
7614
  } else {
7586
- (0, import_build_utils3.debug)("found vendor directory relative to entrypoint");
7615
+ (0, import_build_utils4.debug)("found vendor directory relative to entrypoint");
7587
7616
  }
7588
7617
  await (0, import_fs_extra.move)(relativeVendorDir, vendorDir);
7589
7618
  } else if (hasRootVendorDir) {
7590
- (0, import_build_utils3.debug)("found vendor directory in project root");
7619
+ (0, import_build_utils4.debug)("found vendor directory in project root");
7591
7620
  }
7592
7621
  await (0, import_fs_extra.ensureDir)(vendorDir);
7593
7622
  await bundleInstall(bundlerPath, bundleDir, gemfilePath, rubyPath);
7594
7623
  try {
7595
7624
  await (0, import_fs_extra.remove)((0, import_path3.join)(vendorDir, "cache"));
7596
- } catch (e) {
7625
+ } catch (_e) {
7597
7626
  }
7598
7627
  const originalRbPath = (0, import_path3.join)(__dirname, "..", "vc_init.rb");
7599
7628
  const originalHandlerRbContents = await (0, import_fs_extra.readFile)(originalRbPath, "utf8");
7600
- (0, import_build_utils3.debug)("entrypoint is", entrypoint);
7629
+ const originalUtilsRbPath = (0, import_path3.join)(__dirname, "..", "vc_utils.rb");
7630
+ const originalUtilsRbContents = await (0, import_fs_extra.readFile)(originalUtilsRbPath, "utf8");
7631
+ (0, import_build_utils4.debug)("entrypoint is", entrypoint);
7601
7632
  const userHandlerFilePath = entrypoint.replace(/\.rb$/, "");
7602
7633
  const nowHandlerRbContents = originalHandlerRbContents.replace(
7603
7634
  /__VC_HANDLER_FILENAME/g,
7604
7635
  userHandlerFilePath
7605
7636
  );
7606
7637
  const handlerRbFilename = "vc__handler__ruby";
7638
+ const utilsRbFilename = "vc__utils__ruby.rb";
7607
7639
  const predefinedExcludes = [
7608
7640
  ".git/**",
7609
7641
  ".gitignore",
@@ -7617,10 +7649,13 @@ var build = async ({
7617
7649
  cwd: workPath,
7618
7650
  ignore: predefinedExcludes
7619
7651
  };
7620
- const outputFiles = await (0, import_build_utils3.glob)("**", globOptions);
7621
- outputFiles[`${handlerRbFilename}.rb`] = new import_build_utils3.FileBlob({
7652
+ const outputFiles = await (0, import_build_utils4.glob)("**", globOptions);
7653
+ outputFiles[`${handlerRbFilename}.rb`] = new import_build_utils4.FileBlob({
7622
7654
  data: nowHandlerRbContents
7623
7655
  });
7656
+ outputFiles[utilsRbFilename] = new import_build_utils4.FileBlob({
7657
+ data: originalUtilsRbContents
7658
+ });
7624
7659
  if (config && (config.includeFiles || config.excludeFiles)) {
7625
7660
  const includedPaths = await matchPaths(config.includeFiles, workPath);
7626
7661
  const excludedPaths = await matchPaths(
@@ -7631,7 +7666,7 @@ var build = async ({
7631
7666
  if (includedPaths.includes(excludedPaths[i])) {
7632
7667
  continue;
7633
7668
  }
7634
- if (excludedPaths[i] === `${handlerRbFilename}.rb`) {
7669
+ if (excludedPaths[i] === `${handlerRbFilename}.rb` || excludedPaths[i] === utilsRbFilename) {
7635
7670
  continue;
7636
7671
  }
7637
7672
  if (excludedPaths[i].startsWith(vendorPath)) {
@@ -7640,18 +7675,29 @@ var build = async ({
7640
7675
  delete outputFiles[excludedPaths[i]];
7641
7676
  }
7642
7677
  }
7643
- const output = new import_build_utils3.Lambda({
7678
+ const output = new import_build_utils4.Lambda({
7644
7679
  files: outputFiles,
7645
7680
  handler: `${handlerRbFilename}.vc__handler`,
7646
7681
  runtime,
7647
7682
  environment: {}
7648
7683
  });
7684
+ try {
7685
+ await (0, import_build_utils2.generateRubyProjectManifest)({
7686
+ workPath,
7687
+ gemfileLockPath: (0, import_path3.join)((0, import_path3.dirname)(gemfilePath), "Gemfile.lock"),
7688
+ framework: config?.framework ?? void 0,
7689
+ serviceType: service ? (0, import_build_utils4.getReportedServiceType)(service) : void 0
7690
+ });
7691
+ } catch (err) {
7692
+ (0, import_build_utils4.debug)(`Failed to write ruby manifest: ${String(err)}`);
7693
+ }
7649
7694
  return { output };
7650
7695
  };
7651
7696
  var shouldServe = () => true;
7652
7697
  // Annotate the CommonJS export names for ESM import in node:
7653
7698
  0 && (module.exports = {
7654
7699
  build,
7700
+ diagnostics,
7655
7701
  shouldServe,
7656
7702
  startDevServer,
7657
7703
  version
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.5.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.31.0"
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