jkbrzt / rrule

JavaScript library for working with recurrence rules for calendar dates as defined in the iCalendar RFC and more.
https://jkbrzt.github.io/rrule
Other
3.34k stars 513 forks source link

TypeScript + ESM #478

Closed karlhorky closed 2 years ago

karlhorky commented 3 years ago

Reporting an issue

Thank you for taking an interest in rrule! Please include the following in your report:

rrule@2.6.8 fails with Node.js ESM and TypeScript (using ts-node@10.2.0), both:

  1. Using the CommonJS import syntax, required for importing from CommonJS
import rrule from 'rrule';

const { RRule } = rrule;
Screen Shot 2021-08-13 at 22 46 47

TypeScript error:

Property 'RRule' does not exist on type 'typeof RRule'. ts(2339)

This is because there are only types for the ESM version, which use a default export instead of a named export:

node_modules/rrule/dist/esm/src/rrule.d.ts

export default class RRule implements QueryMethods {
  1. Importing from the ESM entry point
import RRule from 'rrule/dist/esm/src/rrule.js';
$ node --loader ts-node/esm/transpile-only.mjs index.ts
(node:11320) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
/Users/k/p/ical-move-events/node_modules/ts-node/dist-raw/node-esm-resolve-implementation.js:383
    throw new ERR_MODULE_NOT_FOUND(
          ^
CustomError: Cannot find module '/Users/k/p/ical-move-events/node_modules/rrule/dist/esm/src/dateutil' imported from /Users/k/p/ical-move-events/node_modules/rrule/dist/esm/src/rrule.js

Expected output: It should not throw an error when using ESM with TypeScript

Additional details:

macOS Big Sur 11.5.1

karlhorky commented 3 years ago

Workaround

To work around 1. above, use patch-package to patch the line in the types to be a named export:

-export default class RRule implements QueryMethods {
+export class RRule implements QueryMethods {
vaughngit commented 2 years ago

Please commit fix to main branch

karlhorky commented 2 years ago

The fix #507 by @vbudovski almost appears to fix the issue, but it has actually introduced a new bug with the TypeScript types.

import RRule from 'rrule'
console.log(RRule);

This yields this, which almost appears good:

Object [Module] {
  rrulestr: [Getter],
  Frequency: [Getter],
  Weekday: [Getter],
  RRule: [Getter],
  RRuleSet: [Getter],
  default: [Function: RRule] {
    parseText: [Function (anonymous)],
    fromText: [Function (anonymous)],
    fromString: [Function (anonymous)],
    FREQUENCIES: [
      'YEARLY',
      'MONTHLY',
      'WEEKLY',
      'DAILY',
      'HOURLY',
      'MINUTELY',
      'SECONDLY'
    ],
    YEARLY: 0,
    MONTHLY: 1,
    WEEKLY: 2,
    DAILY: 3,
    HOURLY: 4,
    MINUTELY: 5,
    SECONDLY: 6,
    MO: Weekday { weekday: 0, n: undefined },
    TU: Weekday { weekday: 1, n: undefined },
    WE: Weekday { weekday: 2, n: undefined },
    TH: Weekday { weekday: 3, n: undefined },
    FR: Weekday { weekday: 4, n: undefined },
    SA: Weekday { weekday: 5, n: undefined },
    SU: Weekday { weekday: 6, n: undefined },
    parseString: [Function: parseString],
    optionsToString: [Function: optionsToString]
  }
}

However, trying to instantiate the class fails:

console.log(new RRule());
TypeError: RRule is not a constructor
    at file:///Users/k/p/t-rrule/index.ts:4:13
    at ModuleJob.run (node:internal/modules/esm/module_job:183:25)
    at async Loader.import (node:internal/modules/esm/loader:178:24)
    at async Object.loadESM (node:internal/process/esm_loader:68:5)
    at async handleMainPromise (node:internal/modules/run_main:63:12)
error Command failed with exit code 1.

And even accessing the .default or .RRule property yields errors with TypeScript:

console.log(new RRule.default());
console.log(new RRule.RRule());
TSError: ⨯ Unable to compile TypeScript:
index.ts:4:23 - error TS2339: Property 'default' does not exist on type 'typeof RRule'.

4 console.log(new RRule.default());
                        ~~~~~~~

    at createTSError (/Users/k/p/t-rrule/node_modules/ts-node/src/index.ts:843:12)
    at reportTSError (/Users/k/p/t-rrule/node_modules/ts-node/src/index.ts:847:19)
    at getOutput (/Users/k/p/t-rrule/node_modules/ts-node/src/index.ts:1057:36)
    at Object.compile (/Users/k/p/t-rrule/node_modules/ts-node/src/index.ts:1411:41)
    at transformSource (/Users/k/p/t-rrule/node_modules/ts-node/src/esm.ts:400:37)
    at transformSource (/Users/k/p/t-rrule/node_modules/ts-node/src/child/child-loader.ts:24:47)
    at Loader.moduleStrategy (node:internal/modules/esm/translators:141:28)
    at async link (node:internal/modules/esm/module_job:67:21) {
  diagnosticCodes: [ 2339 ]
}
error Command failed with exit code 1.

...

TSError: ⨯ Unable to compile TypeScript:
index.ts:6:23 - error TS2339: Property 'RRule' does not exist on type 'typeof RRule'.

6 console.log(new RRule.RRule());
                        ~~~~~

    at createTSError (/Users/k/p/t-rrule/node_modules/ts-node/src/index.ts:843:12)
    at reportTSError (/Users/k/p/t-rrule/node_modules/ts-node/src/index.ts:847:19)
    at getOutput (/Users/k/p/t-rrule/node_modules/ts-node/src/index.ts:1057:36)
    at Object.compile (/Users/k/p/t-rrule/node_modules/ts-node/src/index.ts:1411:41)
    at transformSource (/Users/k/p/t-rrule/node_modules/ts-node/src/esm.ts:400:37)
    at transformSource (/Users/k/p/t-rrule/node_modules/ts-node/src/child/child-loader.ts:24:47)
    at Loader.moduleStrategy (node:internal/modules/esm/translators:141:28)
    at async link (node:internal/modules/esm/module_job:67:21) {
  diagnosticCodes: [ 2339 ]
}
error Command failed with exit code 1.

So this is not working for TypeScript yet.

karlhorky commented 2 years ago

Workaround with rrule@2.7.0

To work around the type errors in https://github.com/jakubroztocil/rrule/issues/478#issuecomment-1148462507 above, use patch-package to patch the line in node_modules/rrule/dist/esm/src/index.d.ts to remove the default export:

-export default RRule;

Opened #511 for this

vbudovski commented 2 years ago

@karlhorky Does it work if you import it as a named rather than default? i.e. import { RRule } from 'rrule'

karlhorky commented 2 years ago

Nope, fails at runtime in Node.js (with an ESM project using "type": "module") because rrule is not real ESM (see #512):

    import { RRule } from 'rrule';
             ^^^^^
    SyntaxError: Named export 'RRule' not found. The requested module 'rrule' 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 'rrule';
    const { RRule } = pkg;
karlhorky commented 2 years ago

@davidgoli Thanks for merging #513!

Which version will the fix be published in?

karlhorky commented 2 years ago

Looks like it was published in version 2.7.1, and this version indeed resolves the types issue with TypeScript and ESM.

However, since rrule is still a CommonJS-only package, you will get an error if you try to import it with import { RRule } from 'rrule' in an ESM project:

node index.mjs
file:///Users/k/p/ical-move-events/index.mjs:1
import { RRule } from 'rrule';
         ^^^^^
SyntaxError: Named export 'RRule' not found. The requested module 'rrule' 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 'rrule';
const { RRule } = pkg;

    at ModuleJob._instantiate (node:internal/modules/esm/module_job:124:21)
    at async ModuleJob.run (node:internal/modules/esm/module_job:179:5)
    at async Loader.import (node:internal/modules/esm/loader:178:24)
    at async Object.loadESM (node:internal/process/esm_loader:68:5)
    at async handleMainPromise (node:internal/modules/run_main:63:12)

Unfortunately, this means that you still need to do unusual import syntax to make it work:

import pkg from 'rrule';
const { RRule } = pkg;

This issue with rrule not being published as real ESM is tracked in these two issues:

dhornbein commented 2 years ago

I'm having issues related to this (I think) in my Nuxt 3 project. It might have something to do with TypeScript, I'm not sure.

When I do:

import pkg from 'rrule';
const { RRule } = pkg;

I get this error: 'default' is not exported by node_modules/rrule/dist/esm/index.js

When I try and build the nuxt project with the import as it suggests in the documentation I get this error:

[nuxt] [request error] [unhandled] [500] Named export 'rrulestr' not found. The requested module 'rrule' 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:

Weirdly enough when I use my nuxt dev to run a dev environment everything (seemingly) works fine.

This is an awesome package, I really appreciate it.

eytanProxi commented 1 year ago

Same probrem as @dhornbein ... Anyone has a solution ?

caioamaral commented 1 year ago

Same problem here as @eytanProxi and @dhornbein , at v2.7.2.

Did anybody solve this problem?

karlhorky commented 1 year ago

My solution above is working for my project, which is using tsc, tsx and vite:

import pkg from 'rrule';
const { RRule } = pkg;

If you're getting an error about the default export, one thing that you may want to try is the import * as pkg ... syntax:

import * as pkg from 'rrule';
const { RRule } = pkg;
Cilenco commented 1 year ago

For me in my Nuxt3 app worked:

import * as pkg from 'rrule';

import { interopDefault } from 'mlly'
const { RRule } = interopDefault(pkg);
sushantdhiman commented 1 year ago

In my project, for my tests and one-off scripts, I use tsx. For production server builds, I use esbuild. From time-to-time I often encounter some obscure module which will properly compile with either esbuild or tsx. I have got same issue with rrule.

Here is my situation

Works with esbuild

import { Frequency, RRule, RRuleSet } from 'rrule'

Works with tsx

import rrule from 'rrule';

Now I am not sure which one is the culprit here (but I think tsx is the issue). I use following technique to solve this issue

Solution

// rrule/index.cjs

const { Frequency, RRule, RRuleSet } = require('rrule');
module.exports = { Frequency, RRule, RRuleSet };
// rrule/index.cjs.d.ts

import { RRule, RRuleSet, Frequency } from 'rrule';
export { RRule, RRuleSet, Frequency };
// main code

import { RRule, RRuleSet, Frequency } from './rrule/index.cjs'

I hope this helps someone else :)