Skip to content

feat: enable Twoslash on Cloudflare#8837

Draft
dario-piotrowicz wants to merge 1 commit intonodejs:mainfrom
dario-piotrowicz:dario/two-slash-on-cloudflare
Draft

feat: enable Twoslash on Cloudflare#8837
dario-piotrowicz wants to merge 1 commit intonodejs:mainfrom
dario-piotrowicz:dario/two-slash-on-cloudflare

Conversation

@dario-piotrowicz
Copy link
Copy Markdown
Member

Description

Note

Disclaimer, I simply asked open-code to enable twoslashe on Cloudflare and this is what it came with.
It does seem to work and the code makes sense to me

Enables twoslash to work on Cloudflare (previously it would only work on the Vercel deployment):
Screenshot 2026-04-22 at 22 42 31

Validation

See: https://nodejs-website.dario-test.workers.dev/en

Related Issues

Check List

  • I have read the Contributing Guidelines and made commit messages that follow the guideline.
  • I have run pnpm format to ensure the code follows the style guide.
  • I have run pnpm test to check if all tests are passing.
  • I have run pnpm build to check if the website builds without errors.
  • I've covered new added functionality with unit tests if necessary.

Copilot AI review requested due to automatic review settings April 22, 2026 21:59
@dario-piotrowicz dario-piotrowicz requested review from a team as code owners April 22, 2026 21:59
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nodejs-org Ready Ready Preview Apr 22, 2026 10:40pm

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented Apr 22, 2026

PR Summary

Medium Risk
Moderate risk because it changes MDX code rendering/highlighting and adds a build-time generated TypeScript declaration map; failures could break code block rendering or increase build/deploy size on Cloudflare.

Overview
Twoslash is now enabled on Cloudflare deployments. The MDX Shiki setup always turns on Twoslash and, when running on Cloudflare, swaps in a custom VFS-backed twoslasher loaded from a generated twoslash-fsmap.json.

Adds a build step (build:twoslash-fsmap) that generates the virtual filesystem map from TypeScript lib .d.ts files and @types/node, wires it into Turbo’s cloudflare:build:worker dependencies, and ignores apps/site/generated in git. Also updates the @node-core/rehype-shiki Twoslash transformer to accept an injected twoslasher (via createTransformerFactory) to avoid Node.js filesystem assumptions.

Reviewed by Cursor Bugbot for commit a7081c9. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions
Copy link
Copy Markdown
Contributor

👋 Codeowner Review Request

The following codeowners have been identified for the changed files:

Team reviewers: @nodejs/nodejs-website @nodejs/web-infra

Please review the changes when you have a chance. Thank you! 🙏

@dario-piotrowicz
Copy link
Copy Markdown
Member Author

@avivkeller 🙂

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 22, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.84%. Comparing base (a26c508) to head (98310c5).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8837      +/-   ##
==========================================
- Coverage   73.90%   73.84%   -0.06%     
==========================================
  Files         105      105              
  Lines        8889     8889              
  Branches      326      327       +1     
==========================================
- Hits         6569     6564       -5     
- Misses       2319     2324       +5     
  Partials        1        1              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Enables Twoslash-powered code annotations in the Cloudflare Workers deployment by providing a virtual filesystem (VFS) twoslasher and wiring in a build-time generated TypeScript/@types map, while updating the rehype-shiki Twoslash transformer to accept a custom twoslasher.

Changes:

  • Add a VFS-backed Twoslash setup for Cloudflare that loads a pre-generated twoslash-fsmap.json.
  • Introduce a build script (and Turbo task dependency) to generate the VFS map before Cloudflare worker builds.
  • Update @node-core/rehype-shiki’s Twoslash transformer to support options.twoslasher via createTransformerFactory.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/rehype-shiki/src/transformers/twoslash/index.mjs Adds support for passing a custom twoslasher (Cloudflare-compatible path).
apps/site/mdx/plugins.mjs Enables Twoslash on Cloudflare by creating a VFS-based twoslasher and passing it through twoslashOptions.
apps/site/scripts/twoslash-fsmap/index.mjs New script to write the generated VFS map JSON to apps/site/generated/.
apps/site/scripts/twoslash-fsmap/generate.mjs New generator that builds a map of TS lib + @types/node declaration files.
apps/site/turbo.json Adds build:twoslash-fsmap task and makes Cloudflare worker build depend on it.
apps/site/package.json Adds build:twoslash-fsmap script entry.
.gitignore Ignores apps/site/generated/ build artifacts.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +3 to +10
import { writeFileSync } from 'node:fs';

import generateTwoslashFsMap from './generate.mjs';

const fsMap = generateTwoslashFsMap();

writeFileSync(
new URL('../../generated/twoslash-fsmap.json', import.meta.url),
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

writeFileSync will throw in a clean checkout because apps/site/generated/ is not created anywhere before writing twoslash-fsmap.json. Ensure the target directory exists (e.g., mkdirSync(..., { recursive: true })) before writing the file, or write to an existing output directory.

Suggested change
import { writeFileSync } from 'node:fs';
import generateTwoslashFsMap from './generate.mjs';
const fsMap = generateTwoslashFsMap();
writeFileSync(
new URL('../../generated/twoslash-fsmap.json', import.meta.url),
import { mkdirSync, writeFileSync } from 'node:fs';
import generateTwoslashFsMap from './generate.mjs';
const fsMap = generateTwoslashFsMap();
const outputFile = new URL('../../generated/twoslash-fsmap.json', import.meta.url);
mkdirSync(new URL('../../generated/', import.meta.url), { recursive: true });
writeFileSync(
outputFile,

Copilot uses AI. Check for mistakes.
Comment thread apps/site/turbo.json
]
},
"build:twoslash-fsmap": {
"inputs": [],
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turbo task build:twoslash-fsmap is configured with inputs: [], which makes its cache key effectively constant and can cause stale generated/twoslash-fsmap.json to be reused even when the generator script or dependency versions change. Add appropriate inputs (e.g., scripts/twoslash-fsmap/**, and the relevant package.json/lockfile entries) so the task reruns when its true inputs change.

Suggested change
"inputs": [],
"inputs": [
"scripts/twoslash-fsmap/**",
"package.json",
"../../package.json",
"../../pnpm-lock.yaml",
"../../package-lock.json",
"../../yarn.lock",
"../../bun.lockb",
"../../bun.lock"
],

Copilot uses AI. Check for mistakes.
Comment thread apps/site/mdx/plugins.mjs
Comment on lines +40 to +45
compilerOptions: {
moduleResolution: 100,
// Explicitly include @types/node so that the VFS resolves Node.js
// globals and `node:*` module imports from the bundled declarations.
types: ['node'],
},
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moduleResolution: 100 is a hard-coded enum value (Bundler) and is brittle across TypeScript versions/readability. Since ts is available here, prefer moduleResolution: ts.ModuleResolutionKind.Bundler (or the intended enum) to make the intent explicit and avoid relying on numeric constants.

Copilot uses AI. Check for mistakes.
* `twoslash`. This is needed for environments like Cloudflare Workers where
* the filesystem-backed default twoslasher cannot be used.
*
* @param {import('@shikijs/twoslash').TransformerTwoslashOptions} [options]
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc option type here (TransformerTwoslashOptions) is inconsistent with the type used by the caller (TransformerTwoslashIndexOptions in packages/rehype-shiki/src/index.mjs). Align these so editor/TS inference matches the actual options object supported by this helper.

Suggested change
* @param {import('@shikijs/twoslash').TransformerTwoslashOptions} [options]
* @param {import('../../index.mjs').TransformerTwoslashIndexOptions} [options]

Copilot uses AI. Check for mistakes.
Comment on lines +64 to +73
export const twoslash = (options = {}) => {
if (options.twoslasher) {
return createTransformerFactory(
options.twoslasher,
rendererRich(rendererOptions)
)({ ...transformerOptions, ...options });
}

return transformerTwoslash({ ...transformerOptions, ...options });
};
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change adds a new execution path when options.twoslasher is provided (switching from transformerTwoslash to createTransformerFactory). There are existing unit tests for the rehype-shiki package but none covering this branching behavior; add a small unit test that verifies the factory path is used when twoslasher is set (and the default path otherwise) to prevent regressions.

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +52
const tsLibFiles = readdirSync(tsLibDir).filter(
f => f.startsWith('lib.') && /\.d\.([^.]+\.)?[cm]?ts$/i.test(f)
);

for (const file of tsLibFiles) {
fsMap[`/${file}`] = readFileSync(join(tsLibDir, file), 'utf8');
}
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readdirSync() ordering is filesystem-dependent, so the generated fsMap (and resulting JSON) can be non-deterministic across environments. To keep builds reproducible and Turbo caching stable, sort tsLibFiles (and ideally directory entries in collectDtsFiles) before adding them to fsMap.

Copilot uses AI. Check for mistakes.
@dario-piotrowicz dario-piotrowicz marked this pull request as draft April 22, 2026 22:06
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a7081c9. Configure here.

new URL('../../generated/twoslash-fsmap.json', import.meta.url),
JSON.stringify(fsMap),
'utf8'
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing directory creation causes build script failure

High Severity

The writeFileSync call writes to ../../generated/twoslash-fsmap.json, but the generated directory is added to .gitignore and contains no tracked files, so it won't exist on a fresh clone. Node.js's writeFileSync does not create parent directories automatically, meaning the build:twoslash-fsmap script will crash with an ENOENT error. Since cloudflare:build:worker depends on this task, all Cloudflare builds from clean checkouts (including CI) will fail.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a7081c9. Configure here.

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