selfrefactor / rambda

Faster and smaller alternative to Ramda
https://selfrefactor.github.io/rambda
MIT License
1.65k stars 89 forks source link

[BUG] Node cannot resolve `rambda/immutable` import path in js file bundled by rollup+typescript #730

Closed peachest closed 4 months ago

peachest commented 6 months ago

Describe the bug

The following code and configuration is a small demo to reproduce the real problem I met.

I am wrting TypeScript and import Rambda by path rambda/immutable.

After bundling with Rollup, Node cannot resolve and execute the bundled file.

$ node ./dist/index.js 
node:internal/process/esm_loader:34
      internalBinding('errors').triggerUncaughtException(
                                ^

Error [ERR_MODULE_NOT_FOUND]: Cannot find module 'C:\Users\peachest\WebstormProjects\rambda-demo\node_modules\rambda\immutable' imported from C:\Users\peachest\WebstormProjects\rambda-demo\dist\index.js
Did you mean to import rambda-demo/node_modules/rambda/immutable.js?
    at finalizeResolution (node:internal/modules/esm/resolve:264:11)
    at moduleResolve (node:internal/modules/esm/resolve:917:10)
    at defaultResolve (node:internal/modules/esm/resolve:1130:11)
    at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:396:12)
    at ModuleLoader.resolve (node:internal/modules/esm/loader:365:25)
    at ModuleLoader.getModuleJob (node:internal/modules/esm/loader:240:38)
    at ModuleWrap.<anonymous> (node:internal/modules/esm/module_job:85:39)
    at link (node:internal/modules/esm/module_job:84:36) {
  code: 'ERR_MODULE_NOT_FOUND',
  url: 'file:///C:/Users/peachest/WebstormProjects/rambda-demo/node_modules/rambda/immutable'
}

Node.js v20.11.0

Context(which version of library)

version description for key package:

package version
Node.js v20.11.0
typescript v5.4.5
rollup v4.14.3
rambda v9.2.0

My package.json:

{
  "name": "rambda-demo",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "type": "module",
  "scripts": {
    "build": "rollup -cw"
  },
  "devDependencies": {
    "@rollup/plugin-alias": "^5.1.0",
    "@rollup/plugin-commonjs": "^25.0.7",
    "@rollup/plugin-json": "^6.1.0",
    "@rollup/plugin-node-resolve": "^15.2.3",
    "json5": "^2.2.3",
    "rollup": "^4.14.1",
    "rollup-plugin-clear": "^2.0.7",
    "rollup-plugin-copy": "^3.5.0",
    "rollup-plugin-dts": "^6.1.0",
    "rollup-plugin-node-externals": "^7.1.1",
    "rollup-plugin-typescript2": "^0.36.0",
    "typescript": "^5.4.5"
  },
  "dependencies": {
    "rambda": "^9.2.0"
  }
}

My tsconfig.json

{
    "compilerOptions": {
        // Type Checking
        "strict": true,
        // Modules
        "baseUrl": "./",
        "module": "ESNext",
        "moduleResolution": "node",
        "paths": {
            "@/*": [
                "./src/*"
            ],
        },
        "resolveJsonModule": true,
        "rootDir": "./src",
        "outDir": "./dist",
        "allowSyntheticDefaultImports": true,
        "esModuleInterop": true,
        "isolatedModules": true,
        "lib": [
            "esnext"
        ],
        "moduleDetection": "auto",
        "target": "esnext",
    },
    "include": [
        "src/**/*.ts"
    ],
    "exclude": [
        "node_modules",
        "dist"
    ]
}

My rollup.config.js (only key config)

export default {
    input: "src/index.ts",
    output: {
        dir: "./dist",
        format: "es",
        exports: "named",
        preserveModules: true,
    },
    plugins: [
        clear({
            targets: ["./dist"],
        }),
        json(),
        nodeResolve({
            preferBuiltins: false,
        }),
        commonjs(),
        typescript({
            tsconfigJson,
        }),
        externals(),
    ],
};

My ts code

import { T, when } from "rambda/immutable";

when(T, (v) => console.log(v))("Hello, World!")

Bundled js file of Rollup

import { when, T } from 'rambda/immutable';

when(T, (v) => console.log(v))("Hello, World!");

Advice

Instead of using in your package.json:

  "main": "./dist/rambda.js",
  "umd": "./dist/rambda.umd.js",
  "module": "./rambda.js",
  "types": "./index.d.ts",

Use exports field:

  "exports": {
    ".": {
      "import": "./dist/rambda.js",
      "types": "./index.d.ts"
    },
    "./immutable": {
      "import": "./immutable.js",
      "types": "./immutable.d.ts"
    }
  }

Without changing anything of code in my project, Node.js now can resolve rambda/immutable package correctly

$ node ./dist/index.js 
Hello, World!

:warning: I havn't test umd and require.

selfrefactor commented 6 months ago

I will give it a try, but can I ask if the project compiles without immutable?

I will reproduce the bug and see what can it be done. I did test 9.2.0 with TS and it worked. Still, your issue helped me to find that Rambdax has issues regarding latest changes, so in any case, I want to thank you for opening it.

peachest commented 6 months ago

I will give it a try, but can I ask if the project compiles without immutable?

I will reproduce the bug and see what can it be done. I did test 9.2.0 with TS and it worked. Still, your issue helped me to find that Rambdax has issues regarding latest changes, so in any case, I want to thank you for opening it.

Oh,I want to build my project as a lib but not app, so I use rollup-plugin-node-externals to exclude all dependencies.

The full rollup.config.js

import path from "node:path";
import url from "node:url";

import commonjs from "@rollup/plugin-commonjs";
import json from "@rollup/plugin-json";
import nodeResolve from "@rollup/plugin-node-resolve";
import fs from "fs-extra";
import json5 from "json5";
import clear from "rollup-plugin-clear";
import externals from "rollup-plugin-node-externals";
import typescript from "rollup-plugin-typescript2";

const __filename = url.fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const resolve = p => path.resolve(__dirname, p);

const packageJSONPath = resolve("package.json");
const tsconfigJSONPath = resolve("tsconfig.json");
const packageJson = json5.parse(fs.readFileSync(packageJSONPath).toString());
const tsconfigJson = json5.parse(fs.readFileSync(tsconfigJSONPath).toString());

const {
    outDir: outputDirectory
} = tsconfigJson["compilerOptions"];

export default {
    input: "src/index.ts",
    output: {
        dir: "./dist",
        format: "es",
        exports: "named",
        preserveModules: true,
    },
    plugins: [
        clear({
            targets: ["./dist"],
        }),
        json(),
        nodeResolve({
            preferBuiltins: false,
        }),
        commonjs(),
        typescript({
            tsconfigJson,
        }),
        externals(),
    ],
};

The output structure is following

dist
`-- index.js

0 directories, 1 file

and the output index.js only container my project code without Rambda

The above issue is reproduce in this way.


extra problem

I also test not using rollup-plugin-node-externals as rollup plugins, and the output dir structure is

dist
|-- _virtual
|   `-- rambda.js
|-- node_modules
|   `-- rambda
|       |-- dist
|       |   `-- rambda.js
|       `-- immutable.js
`-- src
    `-- index.js

5 directories, 4 files

However,in this cas,execute node ./dist/src/index.js will occur

import { i as immutable } from '../node_modules/rambda/immutable.js';
         ^
SyntaxError: Named export 'i' not found. The requested module '../node_modules/rambda/immutable.js' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

import pkg from '../node_modules/rambda/immutable.js';
const { i: immutable } = pkg;

    at ModuleJob._instantiate (node:internal/modules/esm/module_job:132:21)
    at async ModuleJob.run (node:internal/modules/esm/module_job:214:5)
    at async ModuleLoader.import (node:internal/modules/esm/loader:329:24)
    at async loadESM (node:internal/process/esm_loader:28:7)
    at async handleMainPromise (node:internal/modules/run_main:113:12)

Node.js v20.11.0

But I check the bundled rambda/immutable.js file, which is

import './dist/rambda.js';
import { __exports as rambda } from '../../_virtual/rambda.js';

var immutable = rambda;

export { immutable as i };

And obviously it has exported i

selfrefactor commented 6 months ago

I spent some time exploring options of using exports object in package.json vs using main property and I found that using main is causing less trouble, so I don't think that I will change that. Still, when I have time, I will check what is the root cause for your case.

Just one question - what happens if you don't use immutable as I think this is important to know?

peachest commented 6 months ago

I import rambda by rambda/immutable to use readonly type declaration for typescript. Or do you have some way to use readonly type declaration when coding and compiling without immutable

selfrefactor commented 6 months ago

I mean, is the issue happens if you simply import rambda not rambda/immutable? This will be helpful when debugging.

selfrefactor commented 4 months ago

I am closing this as I needed the last feedback. Feel free to reopen it with answer to it.