mantine-v7 icon indicating copy to clipboard operation
mantine-v7 copied to clipboard

ESM support (particularly in the server, unbundled)

Open gustavopch opened this issue 2 years ago • 10 comments

What package has an issue

All of them.

Describe the bug

I'm installing Mantine in my project that uses Remix v1.18.0.

My package.json contains "type": "module" and my remix.config.js contains serverModuleFormat: 'esm'.

When I run remix dev, I get the error:

SyntaxError: The requested module '@mantine/core' does not provide an export named 'ColorSchemeScript'
    at ModuleJob._instantiate (node:internal/modules/esm/module_job:123:21)
    at ModuleJob.run (node:internal/modules/esm/module_job:189:5)
    at async Promise.all (index 0)
    at ESMLoader.import (node:internal/modules/esm/loader:530:24)
    at Server.<anonymous> (file:///Users/gustavopch/Projects/my-project/server.ts:2:113)

Note that it's not a problem with ColorSchemeScript, it's a problem with ESM in general.

What version of @mantine/hooks page do you have in package.json?

7.0.0-alpha.19

If possible, please include a link to a codesandbox with the reproduced problem

https://codesandbox.io/p/sandbox/lucid-goldwasser-nm7z3k

Do you know how to fix the issue

None

Are you willing to participate in fixing this issue and create a pull request with the fix

None

Possible fix

Adding "type": "module" to Mantine's package.json files makes it work in runtime, but then TypeScript doesn't recognize the types of the imports and starts giving errors like:

Module '"@mantine/core"' has no exported member 'ColorSchemeScript'. ts(2305)

gustavopch avatar Jul 30 '23 01:07 gustavopch

Mantine provides both esm and cjs exports, esm exports are tested to work correctly (tested though Vite which uses only esm). Is there any reason you need to set serverModuleFormat: 'esm' in your application?

rtivital avatar Jul 30 '23 06:07 rtivital

I need serverModuleFormat: 'esm' because my server.ts and the files that are imported by it are all in ESM.

When I remove serverModuleFormat: 'esm' and run remix dev, it crashes when import(build) is called. I think the import() function cannot be used in CJS.

ReferenceError: module is not defined in ES module scope
This file is being treated as an ES module because it has a '.js' file extension and '/Users/gustavopch/Projects/my-project/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
    at file:///Users/gustavopch/Projects/publiko/publiko/build/<stdin>:1:1
    at ModuleJob.run (node:internal/modules/esm/module_job:193:25)
    at async Promise.all (index 0)
    at ESMLoader.import (node:internal/modules/esm/loader:530:24)
    at Server.<anonymous> (file:///Users/gustavopch/Projects/my-project/server.ts:2:113)

gustavopch avatar Jul 30 '23 12:07 gustavopch

I've edited the original message and added this repro which doesn't use Remix at all, just imports something from @mantine/core in a Node.js project that has "type": "module": https://codesandbox.io/p/sandbox/lucid-goldwasser-nm7z3k

gustavopch avatar Jul 30 '23 14:07 gustavopch

Adding "type": "module" to Mantine's package.json files makes it work in runtime, but then TypeScript doesn't recognize the types of the imports and starts giving errors like:

Module '"@mantine/core"' has no exported member 'ColorSchemeScript'. ts(2305)

OK, I know what's happening.

In fact, each of Mantine's package.json files should have "type": "module" so Node.js can import the packages the ESM way. Then the TypeScript error happens because ESM resolves paths in a stricter way – it won't automatically resolve to a folder's index nor resolve the extension (see the TypeScript documentation). So, for example, node_modules/@mantine/core/lib/index.d.ts should be changed:

 export { RemoveScroll } from 'react-remove-scroll';
-export * from './core';
-export * from './components';
+export * from './core/index.js';
+export * from './components/index.js';

The change to the type files is compatible with CJS, but adding "type": "module" would make CJS unable to require('@mantine/core'). I believe that's the reason why Lodash has a separate package lodash-es.

I'm surprised there's no other issue about this yet. Apparently I'm the first one trying to use Mantine to run SSR with ES Modules. 🤔

gustavopch avatar Jul 30 '23 15:07 gustavopch

In case it's useful for anyone, I wrote this script (to be fair, GPT-4 helped me) to patch Mantine:

import glob from 'glob'
import * as fs from 'node:fs'
import * as path from 'node:path'

const filePaths = glob.sync('node_modules/@mantine/**/*.d.ts', {
  absolute: true,
  ignore: '**/@mantine/**/node_modules/**',
})

const importRegex = /from ['"](\.[^'"]+)['"]|import\(['"](\.[^'"]+)['"]\)/g

filePaths.forEach(filePath => {
  const content = fs.readFileSync(filePath, 'utf-8')
  const fileDir = path.dirname(filePath)

  let newContent = content
  let match

  while ((match = importRegex.exec(content)) !== null) {
    const importedPath = match[1] ?? match[2]
    const importedPathRegex = new RegExp(importedPath + '(?=[\'"])')

    for (const [dtsSuffix, jsSuffix] of [
      ['.d.ts', '.js'],
      ['/index.d.ts', '/index.js'],
    ]) {
      if (fs.existsSync(path.join(fileDir, importedPath + dtsSuffix))) {
        newContent = newContent.replace(
          importedPathRegex,
          importedPath + jsSuffix,
        )

        break
      }
    }
  }

  fs.writeFileSync(filePath, newContent)
})

After running it, go into each node_modules/@mantine/* folder, add "type": "module" to its package.json, then create the patch with https://npm.im/patch-package (to prevent package.json from being omitted from the patch, use patch-package --exclude 'nothing' @mantine/core). I haven't tested enough, but I'm leaving it registered here anyway and will edit if needed.

Of course, an official solution would be very much appreciated. Thanks for your good work.

gustavopch avatar Jul 30 '23 20:07 gustavopch

I came up with a hack in shell script that just inserts "type": "module" into those v7 modules. :P

add_type_module() {
  local pkg="./node_modules/$1/package.json"
  grep -q '"type": "module"' $pkg
  grep -q '"type": "module"' $pkg || sed -i "s|\"name\": \"$1\",|\"name\": \"$1\",\n  \"type\": \"module\",|" $pkg
}

# These are all workarounds to support ESM so that we can use top-level await.
esm_hack() {
  sed -i 's/target: "node14",/target: "node20",/g' ./node_modules/\@remix-run/dev/dist/compiler/server/compiler.js
  add_type_module \@mantine/carousel
  add_type_module \@mantine/code-highlight
  add_type_module \@mantine/core
  add_type_module \@mantine/hooks
  add_type_module \@mantine/notifications
  add_type_module \@mantine/spotlight
}

cayter avatar Aug 02 '23 08:08 cayter

@rtivital An easy way to fix the TypeScript part of this issue (which is the most complicated to patch locally) is to add the explicit extension to all imports in the source code, so that tsc will also output the extensions. Some linting would also be needed to prevent regressions. If you want, I can give it a try.

gustavopch avatar Aug 05 '23 20:08 gustavopch

Do you mean that I should change extension to .mjs in package esm folder and to .cjs in cjs folder?

rtivital avatar Aug 06 '23 04:08 rtivital

@rtivital I meant all relative imports should have the full path including the .js extension:

import foo from './some-file'import foo from './some-file.jsimport foo from './some-folder'import foo from './some-folder/index.js

Note that you need to write .js even though the source file is .ts. I know, it's weird, but it's the correct way. This change will be compatible both with CJS and ESM.

But yes, you should also change the extension to .mjs inside the esm folder. So @mantine/core/esm/index.mjs instead of @mantine/core/esm/index.js.

You don't actually need to use .cjs in the cjs folder because the absence of "type": "module" already implies to Node.js that all .js files are CJS, but you can be explicit if you want.

With these changes, I believe ESM will be fully supported.

More info:

  • https://www.typescriptlang.org/docs/handbook/esm-node.html
  • https://antfu.me/posts/publish-esm-and-cjs

Summarizing:

  1. Refactor all source files to use explicit/full paths in all imports (including dynamic imports). Use the .js extension.
  2. Change the dist files inside the esm folder to have the .mjs extension.

gustavopch avatar Aug 06 '23 14:08 gustavopch

Yea I've landed here too, upgrading to Remix v2 and Mantine v7, same issues as what @gustavopch has explained -- Remix by default is ESM now and Mantine isn't quite fully compatible with that.

robertjpayne avatar Sep 18 '23 08:09 robertjpayne