Skip to content

fix(node): avoid dropping api entrypoints with comment-like sequences in strings#16524

Merged
smaeda-ks merged 8 commits into
mainfrom
shohei/fix-filtering
Jun 4, 2026
Merged

fix(node): avoid dropping api entrypoints with comment-like sequences in strings#16524
smaeda-ks merged 8 commits into
mainfrom
shohei/fix-filtering

Conversation

@smaeda-ks

Copy link
Copy Markdown
Member

Summary

Follow-up bug fix for the VERCEL_NODE_FILTER_ENTRYPOINTS entrypoint filtering added in #15873.

isNodeEntrypoint() decides whether a file under api/ is a Vercel Function by stripping comments and then regex-matching for handler export patterns. The comment stripper was not aware of string, template, or regex literals, so a comment-like sequence inside a literal could be misread as a real comment.

In particular, a /* inside a string (e.g. the */* in an Accept header) was treated as the start of a block comment, deleting everything up to the next real */. That frequently swallowed the handler export and silently dropped the function from the build.

Reproduction

module.exports.config = { maxDuration: 60 };

const ACCEPT =
  'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8';

export async function GET() {
  return new Response('Hello from IG!', {
    headers: { 'Accept': ACCEPT },
  }); /* done */
}

The /* inside */* opens a "block comment" that runs until /* done */, deleting the export async function GET declaration. The file no longer matches any export pattern and is dropped.

Fix

Identify handler exports with the es-module-lexer / cjs-module-lexer pair (already dependencies of @vercel/build-utils and already used in the build pipeline, e.g. @vercel/node's dev server) instead of regex-based comment stripping. The lexers are token-aware, so /*, //, and */ inside strings, template literals, and regex literals are never mistaken for comments — eliminating this entire class of false negatives.

The two handler shapes that are not expressible as named exports — module.exports = <fn> and a server that calls .listen() — are matched against comment-stripped source. That stripper now consumes string/template/regex literals as whole tokens, so their contents can't be misread either.

If the ES module lexer cannot parse a file (e.g. a .tsx handler with JSX in its body), detection falls back to the existing safe default (treat as an entrypoint), so a real function is never dropped.

Test plan

  • Added regression tests: */* Accept headers, /* and */ inside string / template / regex literals, plus a genuinely block-commented export (still correctly filtered out).
  • All existing isNodeEntrypoint() unit tests pass (42 total).
  • No behavior change unless VERCEL_NODE_FILTER_ENTRYPOINTS=1.

🤖 Generated with Claude Code

… in strings

isNodeEntrypoint() stripped comments with regexes that were not aware of
string, template, or regex literals, so a `/*` inside a literal (e.g. the
`*/*` in an Accept header) was read as a block-comment start and deleted
everything up to the next real `*/`, swallowing the handler export and
dropping the function under VERCEL_NODE_FILTER_ENTRYPOINTS.

Detect handler exports with the es-module-lexer/cjs-module-lexer pair
already used elsewhere in the build pipeline instead of regex-based
comment stripping; the lexers are token-aware so the contents of literals
are never mistaken for comments. The two handler shapes that are not named
exports (`module.exports = <fn>` and a server that calls `.listen()`) are
matched against comment-stripped source whose stripper now consumes
string/template/regex literals as whole tokens. If the ESM lexer cannot
parse a file (e.g. a .tsx handler with JSX), fall back to the existing
safe default so a real function is never dropped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jun 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8de80b4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
@vercel/build-utils Patch
@vercel/backends Patch
vercel Patch
@vercel/client Patch
@vercel/elysia Patch
@vercel/express Patch
@vercel/fastify Patch
@vercel/fs-detectors Patch
@vercel/gatsby-plugin-vercel-builder Patch
@vercel/h3 Patch
@vercel/hono Patch
@vercel/koa Patch
@vercel/nestjs Patch
@vercel/node Patch
@vercel/static-build Patch
@vercel/cervel Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

🧪 Unit Test Strategy

Comparing: fb30b768de80b4 (view diff)

Strategy: Code changed outside of a package - running all unit tests

⚠️ All unit tests will run because global code changes could impact all packages.

Affected packages - 45 (100%)
  1. @vercel-internals/get-package-json
  2. @vercel/aws
  3. @vercel/backends
  4. @vercel/build-utils
  5. @vercel/cervel
  6. @vercel/cli-auth
  7. @vercel/cli-config
  8. @vercel/cli-exec
  9. @vercel/client
  10. @vercel/config
  11. @vercel/detect-agent
  12. @vercel/edge
  13. @vercel/elysia
  14. @vercel/error-utils
  15. @vercel/express
  16. @vercel/fastify
  17. @vercel/firewall
  18. @vercel/frameworks
  19. @vercel/fs-detectors
  20. @vercel/functions
  21. @vercel/gatsby-plugin-vercel-analytics
  22. @vercel/gatsby-plugin-vercel-builder
  23. @vercel/go
  24. @vercel/h3
  25. @vercel/hono
  26. @vercel/hydrogen
  27. @vercel/koa
  28. @vercel/nestjs
  29. @vercel/next
  30. @vercel/node
  31. @vercel/oidc
  32. @vercel/oidc-aws-credentials-provider
  33. @vercel/python
  34. @vercel/python-analysis
  35. @vercel/redwood
  36. @vercel/related-projects
  37. @vercel/remix-builder
  38. @vercel/routing-utils
  39. @vercel/ruby
  40. @vercel/rust
  41. @vercel/static-build
  42. @vercel/static-config
  43. @vercel/vc-native
  44. examples
  45. vercel

Results

  • Unit tests: All affected packages will run unit tests
  • E2E tests: Running in parallel in this workflow
  • Type checks: All affected packages will run type checks

This comment is automatically generated based on the affected testing strategy

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

📦 CLI Tarball Ready

The Vercel CLI tarball for this PR is now available!

Quick Test

You can test this PR's CLI directly by running:

npx https://vercel-okwzshpm3.vercel.sh/tarballs/vercel.tgz --help

Use in vercel.json

To use this CLI version in your project builds, add to your vercel.json:

{
  "build": {
    "env": {
      "VERCEL_CLI_VERSION": "vercel@https://vercel-okwzshpm3.vercel.sh/tarballs/vercel.tgz"
    }
  }
}

Python Runtime Wheel

A vercel-runtime wheel was also built for this PR.
To use in your Python project builds, also set this environment variable:

VERCEL_RUNTIME_PYTHON="vercel-runtime @ https://vercel-okwzshpm3.vercel.sh/tarballs/vercel_runtime-0.15.0.dev1780587334+09c39af-py3-none-any.whl"

Python Workers Wheel

A vercel-workers wheel was also built for this PR.
To use in your Python project builds, also set this environment variable:

VERCEL_WORKERS_PYTHON="vercel-workers @ https://vercel-okwzshpm3.vercel.sh/tarballs/vercel_workers-0.1.0.dev1780587334+09c39af-py3-none-any.whl"

This comment is automatically generated

…ction

The stripped source is only tested for `module.exports =` and `.listen(`,
which never live inside a literal, so there's no need to preserve string
contents. Blank comments and string/template literals alike with a single
space instead of keeping strings. This drops the conditional replacer and
also removes a class of false positives where literal text such as
"module.exports =" inside a string made a non-entrypoint look like one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…with tests

Move the comment/literal stripper out of node-entrypoint.ts into a dedicated
internal module and compose its matcher from five named, individually-commented
sub-patterns instead of one dense inline regex. The composed pattern is
byte-identical to the previous one (same source and flags), so there is no
behavior change.

Add dedicated unit tests asserting the stripper's exact output on the cases
that motivate it: `//` inside a URL, `/*` inside a string that would otherwise
swallow later code, escaped quotes, template literals, single-quoted strings,
and genuine comments being removed.

The module is not re-exported from index.ts, so it stays out of the package's
public API.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
stripCommentsAndLiterals matched escapes with `\\.`, which does not match a
`\`-newline line continuation, so a string spanning lines via `\` was not
recognized as a single token — a comment-like sequence inside it could then be
read as a real comment and swallow later code. Match escapes with `\\[\s\S]`
instead so escaped newlines are consumed and the literal spans both lines.

The change differs from the previous behavior only on backslash-newline
continuations (verified by a 300k-input fuzz). Add multi-line block-comment,
multi-line template, and line-continuation tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
smaeda-ks and others added 2 commits June 4, 2026 07:47
…ints

`export = handler` (TS, compiles to `module.exports = handler`) and
`export * from './handlers'` (re-exports another module's handlers) are valid
@vercel/node entrypoints, but neither the ES nor CJS lexer surfaces a
recognized handler export for them, so they were being filtered out. Add
textual patterns for both — alongside `module.exports =` and `.listen(` — and
rename the list to EXTRA_HANDLER_PATTERNS. (`export * as ns from` stays
filtered: it exports a namespace object, not a handler.)

Add a coverage suite: a catalog of valid handler shapes (default, named HTTP
methods, fetch, re-exports, CJS forms, export=, export *, servers) and genuine
non-entrypoints, plus a seeded property fuzz asserting a real handler is never
dropped when wrapped in adversarial comment/literal noise.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
stripCommentsAndLiterals could take seconds on malformed input: an
unterminated literal made the matcher fail and re-scan to end-of-input from
every later quote, which is O(n^2) (~8.5s on a 200KB run of escaped quotes).
Entrypoint detection runs on raw source before compilation, so a large or
generated file could stall a build.

Make the closing delimiter optional (`"?`, `'?`, `` `? ``, and `\*\/|$` for
block comments) so an unterminated literal matches in one pass to end-of-input
instead of failing. Valid code is unaffected — every literal is terminated, so
the greedy match still takes the real closing delimiter. Now linear: 1.6M
chars in ~2ms.

Add unterminated-literal tests and a perf-regression guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
unit.node-entrypoint.coverage.test.ts mostly duplicated the spec, so fold its
unique cases into unit.node-entrypoint.test.ts and delete it. The merged file
keeps only the net-new shapes (export=, export * from, object/bracket/
defineProperty CJS exports, default-of-identifier, local export lists,
generics, satisfies, decorators, shebang) and the seeded property fuzz; the
duplicated catalog is dropped. Tests now share one temp file so the fuzz
doesn't spawn thousands of throwaway directories.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@smaeda-ks smaeda-ks marked this pull request as ready for review June 3, 2026 23:03
@smaeda-ks smaeda-ks requested a review from a team as a code owner June 3, 2026 23:03
@smaeda-ks smaeda-ks requested a review from jeffsee55 June 3, 2026 23:03
@smaeda-ks smaeda-ks merged commit 09c39af into main Jun 4, 2026
680 of 683 checks passed
@smaeda-ks smaeda-ks deleted the shohei/fix-filtering branch June 4, 2026 15:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants