shikijs / twoslash

You take some Shiki, add a hint of TypeScript compiler, and 🎉 incredible static code samples
https://shikijs.github.io/twoslash/
MIT License
1.06k stars 51 forks source link

TypeError: `attacher.call` is not a function when using `remark-shiki-twoslash` with `unified` #147

Open mattcroat opened 2 years ago

mattcroat commented 2 years ago

Hey! 👋

I'm using remark-shiki-twoslash with unified and I get this error TypeError: attacher.call is not a function. I already opened a discussion at https://github.com/unifiedjs/unified/discussions/187 but I was told to raise the issue here.

Here is the reproduction on StackBlitz. I have a simple Express server without TypeScript and I use "type": "module" in package.json.

// index.js

import { unified } from 'unified'
import shikiTwoslash from 'remark-shiki-twoslash'
import parseMarkdown from 'remark-parse'
import serializeMarkdown from 'remark-stringify'
import markdownToHtml from 'remark-rehype'
import markdownToHtmlAgain from 'rehype-raw'
import serializeHtml from 'rehype-stringify'

const result = await unified()
  .use(parseMarkdown)
  .use(serializeMarkdown)
  .use([
    [shikiTwoslash, { theme: 'dark-plus' }]
    // ...
  ])
  .use(markdownToHtml, { allowDangerousHtml: true })
  .use(markdownToHtmlAgain)
  .use(serializeHtml)
  .process('```js \n console.log("Hello, World!") \n ```')
> server@ start /server
> node ./src/index.js

file:///server/node_modules/unified/lib/index.js:136
      const transformer = attacher.call(processor, ...options)
                                   ^

TypeError: attacher.call is not a function
    at Function.freeze (file:///server/node_modules/unified/lib/index.js:136:36)
    at Function.process (file:///server/node_modules/unified/lib/index.js:375:15)
    at output (file:///server/src/index.js:25:4)
    at file:///server/src/index.js:35:20
    at ModuleJob.run (node:internal/modules/esm/module_job:197:25)
    at async Promise.all (index 0)
    at async ESMLoader.import (node:internal/modules/esm/loader:337:24)
    at async loadESM (node:internal/process/esm_loader:88:5)
    at async handleMainPromise (node:internal/modules/run_main:61:12)
orta commented 2 years ago

Interesting, perhaps there's different constraints when running inside a module - I don't have any code which runs in ESM node, so I've not hit it. Open to PRs though 👍🏻

chopfitzroy commented 2 years ago

Heya @mattcroat

Can you try using:

.use([
    [shikiTwoslash.default, { theme: 'dark-plus' }]
    // ...
  ])

Have updated your example as well: https://stackblitz.com/edit/node-shiki-twoslash-tcez4v and it appears to be working as expected.

I ran into a similar issue using remark-shiki-twoslash with Astro.

enpitsuLin commented 1 year ago

@chopfitzroy ’s advice is useful Snipaste_2022-12-06_18-17-05

I Think remark-shiki-twoslash maybe should be a pure ESM library

arcanis commented 1 year ago

It occurs because you list the esm entrypoint through the module key, which Node doesn't actually use when resolving import calls (it only reads the exports field). As a result, the cjs version of the library is loaded and, while Node tries to shim it to look like an ESM module, it doesn't do it perfectly and messes up the translation, causing the default export to be unnecessarily wrapped.

To fix it, you have to list in the package.json something akin to:

{
  "main": "./dist/index.js",
  "module": "./dist/remark-shiki-twoslash.esm.mjs",
  "typings": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/remark-shiki-twoslash.esm.mjs",
      "types": "./dist/index.d.ts",
      "default": "./dist/index.js"
    }
  }
}

Note that the esm file also needs to have its extension be .mjs, not .js - otherwise Node will complain and the import will fail. That's why I didn't submit a PR - this stuff seems to be handled by TSDX, and I don't know if it supports configuring that.

Levent0z commented 1 year ago

When I use the .default workaround, nextjs site builds, but all triple-tick blocks are removed from the resulting HTML and appear to be replaced with <!-- -->. This is my next.config.mjs:

const nextConfig = {
    reactStrictMode: true,
    webpack: (config, options) => {
        config.module.rules.push({
            test: /\.mdx?$/,
            use: [
                options.defaultLoaders.babel,
                {
                    loader: '@mdx-js/loader',
                    options: {
                        providerImportSource: '@mdx-js/react',
                        remarkPlugins: [
                            remarkFrontmatter,
                            [remarkShikiTwoslash.default, {
                                themes: ['dark-plus', 'light-plus'],
                                alwayRaiseForTwoslashExceptions: true,
                                // https://github.com/shikijs/twoslash/issues/131
                                disableImplicitReactImport: true,
                            }],
                            remarkGfm,
                            remarkMath,
                        ],
                        rehypePlugins: [
                            [rehypeKatex, { throwOnError: true, strict: true, output: 'mathml' }],
                            rehypeSlug,
                            [rehypeAutolinkHeadings, { behavior: 'wrap' }],
                            // [withShiki, { highlighter }],
                            [template, { template: wrapTemplate }]
                        ],
                    },
                },
            ],
        });
        // Fixes npm packages (mdx) that depend on `fs` module
        if (!options.isServer) {
            config.resolve.fallback.fs = false
        }
        return config;
    },
    pageExtensions: ['js', 'jsx', 'tsx', 'md', 'mdx'],
    images: {
        loader: 'imgix',
        path: 'https://images.unsplash.com/',
    },
};