fix(node): avoid dropping api entrypoints with comment-like sequences in strings#16524
Conversation
… 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 detectedLatest commit: 8de80b4 The changes in this PR will be included in the next version bump. This PR includes changesets to release 16 packages
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 |
🧪 Unit Test StrategyComparing: Strategy: Code changed outside of a package - running all unit tests Affected packages - 45 (100%)
Results
This comment is automatically generated based on the affected testing strategy |
📦 CLI Tarball ReadyThe Vercel CLI tarball for this PR is now available! Quick TestYou can test this PR's CLI directly by running: npx https://vercel-okwzshpm3.vercel.sh/tarballs/vercel.tgz --helpUse in vercel.jsonTo use this CLI version in your project builds, add to your {
"build": {
"env": {
"VERCEL_CLI_VERSION": "vercel@https://vercel-okwzshpm3.vercel.sh/tarballs/vercel.tgz"
}
}
}Python Runtime WheelA Python Workers WheelA 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>
…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>
Summary
Follow-up bug fix for the
VERCEL_NODE_FILTER_ENTRYPOINTSentrypoint filtering added in #15873.isNodeEntrypoint()decides whether a file underapi/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 anAcceptheader) 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
The
/*inside*/*opens a "block comment" that runs until/* done */, deleting theexport async function GETdeclaration. The file no longer matches any export pattern and is dropped.Fix
Identify handler exports with the
es-module-lexer/cjs-module-lexerpair (already dependencies of@vercel/build-utilsand 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
.tsxhandler 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
*/*Acceptheaders,/*and*/inside string / template / regex literals, plus a genuinely block-commented export (still correctly filtered out).isNodeEntrypoint()unit tests pass (42 total).VERCEL_NODE_FILTER_ENTRYPOINTS=1.🤖 Generated with Claude Code