Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Usage with Next.js SSR #138

Closed
schickling opened this issue Feb 25, 2021 · 14 comments
Closed

Usage with Next.js SSR #138

schickling opened this issue Feb 25, 2021 · 14 comments
Labels
invalid This doesn't seem right

Comments

@schickling
Copy link

First of all: Thanks so much for creating this great library. I've spend too many hours trying to configured Monaco to get VSC themes working before I found Shiki. 😅

I'm trying to use Shiki in a Next.js app using Next.js' SSR feature via getStaticProps. Everything works great during local development (via next dev) however the app breaks in production after running next build (which bundled the app) resulting in errors like these:

image

The reason for this is that Shiki dynamically tries to access the theme/grammar files using the fs Node package. However these resources are no longer available after the next build step. Next.js uses nft to automatically detect and bundle assets. It would be great if Shiki could access the required assets in a way that nft can understand it and it therefore works with Next.js.

As a temporary workaround I'm doing the following:

const theme = require('shiki/themes/github-light.json')
const grammar = require('shiki/languages/tsx.tmLanguage.json')

const highlighter = await getHighlighter({
  theme,
  langs: [{ id: 'tsx', scopeName: 'source.tsx', grammar }],
})
@odex21
Copy link

odex21 commented Mar 4, 2021

I had a similar problem when I used with vitesse.
Your workaround is worked for me.

@octref
Copy link
Collaborator

octref commented Mar 8, 2021

I'm not very familiar with Next.js, but I gave it a try. next build and next start seems to work fine, can you tell me what's different in your setup?

octref/next-shiki@dd83217

@kennetpostigo
Copy link

@octref do you know if there is a way to get mdx in nextjs to pick use shiki highlighting?

@octref
Copy link
Collaborator

octref commented Mar 30, 2021

@kennetpostigo You might need to write a remark plugin: https://nextjs-prism.vercel.app/prism

@THiragi
Copy link

THiragi commented Apr 8, 2021

I had the same problem with my Next.js app (and dealt with it in the same way).

Everything works fine in local using next build and next start. I believe that bundled the app access the theme/language files in local node_modules. So it doesn't work in the production environment without node_modules (e.g. Vercel).

Even in local production server started with next start, if you do npm uninstall shiki after next build, rendering with Shiki will fail. (This means that theme/language files are not included in bundled the app.)

@octref
Copy link
Collaborator

octref commented Apr 8, 2021

@THiragi The currently recommended way to include language/theme JSON files are not bundling them, but serving them as static assets.

@octref octref added the invalid This doesn't seem right label Jun 24, 2021
@octref
Copy link
Collaborator

octref commented Jun 24, 2021

See octref/next-shiki@dd83217 for next.js usage. If you still have issues please open a new one.

@octref octref closed this as completed Jun 24, 2021
@thien-do
Copy link

thien-do commented Mar 2, 2022

See octref/next-shiki@dd83217 for next.js usage. If you still have issues please open a new one.

@octref the next-shiki repo will always work, because the Shiki highlighting is done at getStaticProps, which is run at build-time, which has the full node_modules there.

However, I think it's not what @schickling and others are asking for in this issue. They are asking for SSR, which is run-time, and on server-side.

(basically in next-js there are 3 states:

  • build-time, on server -> this is getStaticProps, your example
  • run-time, on server -> this is getServerSideProps, or getStaticProps in case of ISR, which is this issue
  • run-time, on client -> not those getXXX at all)

That being said, I think I successfully have Shiki on getServerSideProps! This means:

  • ALL languages are available
  • ALL themes are available
  • NONE of them are bundled into client side

The trick is:

  • Copy shiki/themes and shiki/languages to somewhere outside of node_modules, maybe under lib/shiki
  • Touch these folders in a server side function (e.g. fs.readdirSync)
  • Done!

The key here is to let Vercel nft to know about the existence of Shiki/themes and shiki/languages so they are included in the production run-time

Sample code: https://github.com/thien-do/memos.pub/blob/a3babb1f149f05c43012278331f885d81f5fcfac/lib/mdx/plugins/code.ts

@ng-hai
Copy link

ng-hai commented May 14, 2022

It’s great if Shiki allows to fetch languages and themes from CDNs like unpkg.com/shiki even in server side.

@rfoel
Copy link

rfoel commented Oct 19, 2022

I'm using remix and had the same problem. I'm using it with serverless with esbuild and I had to mark shiki as an external module to bundle it with my application.
Also, this comment explains well what's going on.

@sreetamdas
Copy link

For anyone looking for a run-time, client-side solution to use Shiki in Next.js, this is now possible with setWasm and setCDN.

To be clear, this allows running Shiki on the client-side, which will add ~137kB for onig.wasm and an additional ~15-20kB per language grammar. You most likely want to use it on the server during run-time instead: and for this @thien-do's solution above works well enough, or you can check out this official example: https://github.com/shikijs/next-shiki


Anyway, here's what you can do: (I used https://github.com/shikijs/shiki-playground as my main point of reference)

  • Because Shiki needs https://github.com/microsoft/vscode-oniguruma for tokenization, we'll need to make sure Shiki has access to it on the client side.
  • To do this, add a script in your package.json to copy over everything that Shiki needs, like so:
{
  "scripts": {
    "copy": "mkdir -p public/shiki && cp -r node_modules/shiki/{dist,languages,samples,themes} public/shiki/"
  }
}
  • Make sure to run copy in both your dev and build scripts — you can also use predev and prebuild scripts for this.
  • Wherever you're using getHighlighter(), add the following so Shiki knows the source for loading the oniguruma web assembly module, and the path for loading other assets (language grammars):
import { getHighlighter, setWasm, setCDN, Lang } from "shiki";

const preloadedLangs: Array<Lang> = ["js", "jsx", "ts", "tsx", "elixir"];
export async function getClientSideHighlighter() {
+  setWasm("/shiki/dist/onigasm.wasm");
+  setCDN("/shiki/");

  const highlighter = await getHighlighter({ theme: "poimandres", langs: preloadedLangs });
  return highlighter;
}

And that's mostly it: you can now use Shiki to get your tokenized HTML and put it inside a dangerouslySetInnerHTML.

@alivault
Copy link

See octref/next-shiki@dd83217 for next.js usage. If you still have issues please open a new one.

@octref the next-shiki repo will always work, because the Shiki highlighting is done at getStaticProps, which is run at build-time, which has the full node_modules there.

However, I think it's not what @schickling and others are asking for in this issue. They are asking for SSR, which is run-time, and on server-side.

(basically in next-js there are 3 states:

  • build-time, on server -> this is getStaticProps, your example
  • run-time, on server -> this is getServerSideProps, or getStaticProps in case of ISR, which is this issue
  • run-time, on client -> not those getXXX at all)

That being said, I think I successfully have Shiki on getServerSideProps! This means:

  • ALL languages are available
  • ALL themes are available
  • NONE of them are bundled into client side

The trick is:

  • Copy shiki/themes and shiki/languages to somewhere outside of node_modules, maybe under lib/shiki
  • Touch these folders in a server side function (e.g. fs.readdirSync)
  • Done!

The key here is to let Vercel nft to know about the existence of Shiki/themes and shiki/languages so they are included in the production run-time

Sample code: https://github.com/thien-do/memos.pub/blob/a3babb1f149f05c43012278331f885d81f5fcfac/lib/mdx/plugins/code.ts

This is the answer. Thank you so much. I finally got Rehype Pretty Code / Shiki working on Vercel thanks to your answer.

@JoshMerlino
Copy link

Im running NextJS 13.5 and using the app router. Nothing I tried here was working, Vercel refused to see the shiki languages regardless of what I did. After slamming my head for a week, I realized Vercel can download the stuff it needs during runtime in order to work with SSR.

This is a modified getHighlighter function which will download the theme and languages to the /tmp dir on Vercel.

import { existsSync } from "fs";
import { writeFile } from "fs/promises";
import { mkdirp } from "mkdirp";
import { tmpdir } from "os";
import { dirname, join } from "path";
import type { HighlighterOptions, ILanguageRegistration, Theme } from "shiki";
import { BUNDLED_LANGUAGES, getHighlighter as shiki_getHighlighter } from "shiki";

export async function getHighlighter({ theme, langs: _langs = BUNDLED_LANGUAGES.map(a => a.id), ...options }: Omit<HighlighterOptions, "langs" | "themes"> & { langs?: Array<ILanguageRegistration | string>, theme: Theme }) {

	// Ensure the tmp directory exists
	const TMP = process.env.VERCEL ? "/tmp" : tmpdir();
	
	// Convert all strings to languages
	const langs = _langs.map(lang => typeof lang === "string" ? BUNDLED_LANGUAGES.find(({ id, aliases }) => aliases?.map(a => a.toLowerCase()).includes(lang.toLowerCase()) || id === lang) : lang) as ILanguageRegistration[];

	// Get all dependencies
	const deps = langs.flatMap(lang => (lang.embeddedLangs || []).map(a => BUNDLED_LANGUAGES.find(b => b.id === a)));

	return await shiki_getHighlighter({

		...options,

		paths: { languages: TMP },

		// Download the theme
		theme: await fetch(`https://unpkg.com/shiki/themes/${ theme }.json`, { redirect: "follow" })
			.then(res => res.json()),
										
		// Download all languages
		langs: await Promise.allSettled(Array.from(new Set([ ...langs, ...deps ])).map(async function(lang) {
			if (!lang) return;
											
			// Get output path
			const path = join(TMP, `${ lang.id }.tmLanguage.json`);
			if (!existsSync(dirname(path))) await mkdirp(dirname(path));
											
			// If the file exists, return it
			if (existsSync(path)) return { ...lang, path };
			
			await fetch(`https://unpkg.com/shiki/languages/${ lang.id }.tmLanguage.json`, { redirect: "follow" })
				.then(res => res.text())
				.then(grammar => writeFile(path, grammar, "utf-8"));
											
			return { ...lang, path };
											
		}))
			.then(langs => langs.filter(({ status }) => status === "fulfilled"))
			.then(langs => (langs as { value: ILanguageRegistration }[]).map(({ value }) => value)),
		
	});
	
}

Then with NextJS App Router, I can use

export default async function Page() {
	const code = `\
import Link from "next/link";
import { Card } from "nextui/Card";
import ogs from "open-graph-scraper";
 
export async function OpenGraphLink({ href }: { href: string; }) {
	const { result, error } = await ogs({ url: href });
	if (error) return null;
 
	const url = new URL(result.requestUrl || result.ogUrl || href);
	const name = result.twitterTitle || result.ogSiteName || result.ogTitle || url.hostname;
	const banner = result.ogImage?.[0]?.url || result.twitterImage?.[0]?.url;
	const description = result.ogDescription || result.twitterDescription;
	const favicon = (result.favicon?.startsWith("/") ? \`\${ url.protocol }//\${ url.hostname }/\${ result.favicon }\` : result.favicon) || \`\${ url.protocol }//\${ url.hostname }/favicon.ico\`;
 
	return (
		<Link href={ url.toString() } target="_blank">
			<Card className="p-0 gap-0 hover:shadow-lg dark:hover:shadow-xl hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-all">
				{banner && <img alt={ name } className="object-cover m-[1px] rounded-md" src={ banner } />}
				<div className="flex gap-4 items-center mx-4 my-2">
					<div className="rounded-full bg-gray-100 flex items-center justify-center w-10 aspect-square shrink-0">
						<img alt={ name } height={ 32 } src={ favicon } width={ 32 } />
					</div>
					<div className="flex flex-col relative overflow-hidden max-w-full">
						<h1 className="text-lg font-medium text-gray-800 dark:text-gray-200 truncate -mb-0.5">{name}</h1>
						<p className="text-sm whitespace-normal">{\`\${ url.hostname }\${ url.port }\${ url.pathname }\`.replace(/\\/$/, "")}</p>
					</div>
				</div>
				<p className="!p-0 !px-4 !pb-2.5 text-sm text-gray-800 dark:text-gray-200">{description}</p>
			</Card>
		</Link>
	);
}`;
	
	const highlighter = await getHighlighter({ theme: "github-dark", langs: [ "tsx" ]});
	const __html = highlighter.codeToHtml(code, "tsx");

	return <div className="[&>pre]:whitespace-pre-wrap" dangerouslySetInnerHTML={{ __html }} />;

}

Want to use it with MDX?

rehypePlugins: [
        [ rehypePrettyCode, {
                getHighlighter: ({ theme }: { theme: Theme }) => getHighlighter({ theme, langs: [ "tsx" ]})
        } ]
]

And yes this stupid hack actually does work... I'm not calling this a production solution, just a lil hack until Vercel figures this out. https://next-base-git-tests-joshmerlino.vercel.app/shiki

@clearly-outsane
Copy link

It didn't work for me using app router in a turborepo either. Gonna try this instead ^

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
invalid This doesn't seem right
Projects
None yet
Development

No branches or pull requests