Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions apps/site/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ const nextConfig = {
},
],
},
serverExternalPackages: ['twoslash'],
outputFileTracingIncludes: {
// Twoslash needs TypeScript declarations to function, and, by default,
// Next.js strips them for brevity. Therefore, they must be explicitly
// included.
'/*': [
'../../node_modules/.pnpm/typescript@*/node_modules/typescript/lib/*.d.ts',
'./node_modules/@types/node/**/*',
],
},
// On static export builds we want the output directory to be "build"
distDir: ENABLE_STATIC_EXPORT ? 'build' : undefined,
// On static export builds we want to enable the export feature
Expand Down
19 changes: 18 additions & 1 deletion apps/site/next.mdx.plugins.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,24 @@ export const REHYPE_PLUGINS = [
[rehypeAutolinkHeadings, { behavior: 'wrap' }],
// Transforms sequential code elements into code tabs and
// adds our syntax highlighter (Shikiji) to Codeboxes
rehypeShikiji,
[
rehypeShikiji,
{
twoslash: true,
// We use the faster WASM engine on the server instead of the web-optimized version.
//
// Currently we fall back to the JavaScript RegEx engine
// on Cloudflare workers because `shiki/wasm` requires loading via
// `WebAssembly.instantiate` with custom imports, which Cloudflare doesn't support
// for security reasons.
//
// TODO(@avivkeller): When available, use `OPEN_NEXT_CLOUDFLARE` environment
// variable for detection instead of current method, which will enable better
// tree-shaking.
// Reference: https://github.com/nodejs/nodejs.org/pull/7896#issuecomment-3009480615
wasm: !('Cloudflare' in global),
},
],
];

/**
Expand Down
1 change: 1 addition & 0 deletions apps/site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"semver": "~7.7.2",
"sval": "^0.6.3",
"tailwindcss": "catalog:",
"twoslash": "^0.3.4",
"vfile": "~6.0.3",
"vfile-matter": "~5.0.1"
},
Expand Down
5 changes: 3 additions & 2 deletions apps/site/pages/en/learn/typescript/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ These type definitions allow TypeScript to understand Node.js APIs and provide p
```js
import * as fs from 'fs';

fs.readFile('example.txt', 'foo', (err, data) => {
// ^^^ Argument of type '"foo"' is not assignable to parameter of type …
fs.readFile('example.txt', foo, (err, data) => {
// ^^^
// Argument of type '"foo"' is not assignable to parameter of type …
if (err) {
throw err;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,9 @@ A note about directory organisation: There are a few common practices for placin
The purpose of types is to warn an implementation will not work:

```ts
// @errors: 2322
const foo = 'a';
const bar: number = 1 + foo;
// ^^^ Type 'string' is not assignable to type 'number'.
```

TypeScript has warned that the above code will not behave as intended, just like a unit test warns that code does not behave as intended. They are complementary and verify different things—you should have both.
Expand Down
28 changes: 1 addition & 27 deletions apps/site/pages/en/learn/typescript/transpile.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ If you have type errors in your TypeScript code, the TypeScript compiler will ca
We will modify our code like this, to voluntarily introduce a type error:

```ts
// @errors: 2322 2554
type User = {
name: string;
age: number;
Expand All @@ -88,31 +89,4 @@ const justine: User = {
const isJustineAnAdult: string = isAdult(justine, "I shouldn't be here!");
```

And this is what TypeScript has to say about this:

```console
example.ts:12:5 - error TS2322: Type 'string' is not assignable to type 'number'.

12 age: 'Secret!',
~~~

example.ts:3:5
3 age: number;
~~~
The expected type comes from property 'age' which is declared here on type 'User'

example.ts:15:7 - error TS2322: Type 'boolean' is not assignable to type 'string'.

15 const isJustineAnAdult: string = isAdult(justine, "I shouldn't be here!");
~~~~~~~~~~~~~~~~

example.ts:15:51 - error TS2554: Expected 1 arguments, but got 2.

15 const isJustineAnAdult: string = isAdult(justine, "I shouldn't be here!");
~~~~~~~~~~~~~~~~~~~~~~


Found 3 errors in the same file, starting at: example.ts:12
```

As you can see, TypeScript is very helpful in catching bugs before they even happen. This is one of the reasons why TypeScript is so popular among developers.
1 change: 1 addition & 0 deletions apps/site/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
*/

@import '@node-core/ui-components/styles/index.css';
@import '@node-core/rehype-shiki/twoslash.css';
@import './locales.css';
4 changes: 3 additions & 1 deletion packages/rehype-shiki/package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{
"name": "@node-core/rehype-shiki",
"version": "1.1.0",
"version": "1.2.0",
"type": "module",
"exports": {
".": "./src/index.mjs",
"./twoslash.css": "./src/twoslash.css",
"./*": "./src/*.mjs"
},
"repository": {
Expand All @@ -23,6 +24,7 @@
"@shikijs/core": "^3.12.0",
"@shikijs/engine-javascript": "^3.12.0",
"@shikijs/engine-oniguruma": "^3.12.0",
"@shikijs/twoslash": "^3.12.2",
"classnames": "catalog:",
"hast-util-to-string": "^3.0.1",
"shiki": "~3.12.0",
Expand Down
28 changes: 1 addition & 27 deletions packages/rehype-shiki/src/__tests__/highlighter.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,7 @@ mock.module('shiki/themes/nord.mjs', {
});

describe('createHighlighter', async () => {
const { createHighlighter } = await import('../highlighter.mjs');

describe('getLanguageDisplayName', () => {
it('returns display name for known languages', () => {
const langs = [
{ name: 'javascript', displayName: 'JavaScript', aliases: ['js'] },
];
const highlighter = createHighlighter({ langs });

assert.strictEqual(
highlighter.getLanguageDisplayName('javascript'),
'JavaScript'
);
assert.strictEqual(
highlighter.getLanguageDisplayName('js'),
'JavaScript'
);
});

it('returns original name for unknown languages', () => {
const highlighter = createHighlighter({ langs: [] });
assert.strictEqual(
highlighter.getLanguageDisplayName('unknown'),
'unknown'
);
});
});
const { default: createHighlighter } = await import('../highlighter.mjs');

describe('highlightToHtml', () => {
it('extracts inner HTML from code tag', () => {
Expand Down
10 changes: 5 additions & 5 deletions packages/rehype-shiki/src/__tests__/plugin.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { describe, it, mock } from 'node:test';

// Simplified mocks - only mock what's actually needed
mock.module('../index.mjs', {
namedExports: { highlightToHast: mock.fn(() => ({ children: [] })) },
defaultExport: () => ({ highlightToHast: mock.fn(() => ({ children: [] })) }),
});

mock.module('classnames', {
Expand All @@ -23,13 +23,13 @@ describe('rehypeShikiji', async () => {
const { default: rehypeShikiji } = await import('../plugin.mjs');
const mockTree = { type: 'root', children: [] };

it('calls visit twice', () => {
it('calls visit twice', async () => {
mockVisit.mock.resetCalls();
rehypeShikiji()(mockTree);
await rehypeShikiji()(mockTree);
assert.strictEqual(mockVisit.mock.calls.length, 2);
});

it('creates CodeTabs for multiple code blocks', () => {
it('creates CodeTabs for multiple code blocks', async () => {
const parent = {
children: [
{
Expand Down Expand Up @@ -61,7 +61,7 @@ describe('rehypeShikiji', async () => {
}
});

rehypeShikiji()(mockTree);
await rehypeShikiji()(mockTree);
assert.ok(parent.children.some(child => child.tagName === 'CodeTabs'));
});
});
52 changes: 28 additions & 24 deletions packages/rehype-shiki/src/highlighter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,47 @@ import shikiNordTheme from 'shiki/themes/nord.mjs';

const DEFAULT_THEME = {
// We are updating this color because the background color and comment text color
// in the Codebox component do not comply with accessibility standards
// @see https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html
// in the Codebox component do not comply with accessibility standards.
// See: https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html
colorReplacements: { '#616e88': '#707e99' },
...shikiNordTheme,
};

export const getLanguageByName = (language, langs) =>
langs.find(
({ name, aliases }) =>
name.toLowerCase() === language.toLowerCase() ||
(aliases !== undefined && aliases.includes(language.toLowerCase()))
);

/**
* Creates a syntax highlighter with utility functions
* @param {import('@shikijs/core').HighlighterCoreOptions} options - Configuration options for the highlighter
* Factory function to create a syntax highlighter instance with utility methods.
*
* @param {Object} params - Parameters for highlighter creation.
* @param {import('@shikijs/core').HighlighterCoreOptions} [params.coreOptions] - Core options for the highlighter.
* @param {import('@shikijs/core').CodeToHastOptions} [params.highlighterOptions] - Additional options for highlighting.
*/
export const createHighlighter = options => {
const shiki = createHighlighterCoreSync({
const createHighlighter = ({ coreOptions = {}, highlighterOptions = {} }) => {
const options = {
themes: [DEFAULT_THEME],
...options,
});
...coreOptions,
};

const theme = options.themes?.[0] ?? DEFAULT_THEME;
const langs = options.langs ?? [];
const shiki = createHighlighterCoreSync(options);

const getLanguageDisplayName = language => {
const languageByIdOrAlias = langs.find(
({ name, aliases }) =>
name.toLowerCase() === language.toLowerCase() ||
(aliases !== undefined && aliases.includes(language.toLowerCase()))
);

return languageByIdOrAlias?.displayName ?? language;
};
const theme = options.themes[0];

/**
* Highlights code and returns the inner HTML inside the <code> tag
*
* @param {string} code - The code to highlight
* @param {string} lang - The programming language to use for highlighting
* @param {Record<string, any>} meta - Metadata
* @returns {string} The inner HTML of the highlighted code
*/
const highlightToHtml = (code, lang) =>
const highlightToHtml = (code, lang, meta = {}) =>
shiki
.codeToHtml(code, { lang, theme })
.codeToHtml(code, { lang, theme, meta, ...highlighterOptions })
// Shiki will always return the Highlighted code encapsulated in a <pre> and <code> tag
// since our own CodeBox component handles the <code> tag, we just want to extract
// the inner highlighted code to the CodeBox
Expand All @@ -52,14 +54,16 @@ export const createHighlighter = options => {
*
* @param {string} code - The code to highlight
* @param {string} lang - The programming language to use for highlighting
* @param {Record<string, any>} meta - Metadata
*/
const highlightToHast = (code, lang) =>
shiki.codeToHast(code, { lang, theme });
const highlightToHast = (code, lang, meta = {}) =>
shiki.codeToHast(code, { lang, theme, meta, ...highlighterOptions });

return {
shiki,
getLanguageDisplayName,
highlightToHtml,
highlightToHast,
};
};

export default createHighlighter;
Loading
Loading