diff --git a/apps/demos/Demos/Charts/MultipleAxes/Vue/App.vue b/apps/demos/Demos/Charts/MultipleAxes/Vue/App.vue index 65d5d0ba4d27..31e03cbb1fab 100644 --- a/apps/demos/Demos/Charts/MultipleAxes/Vue/App.vue +++ b/apps/demos/Demos/Charts/MultipleAxes/Vue/App.vue @@ -70,7 +70,7 @@ function customizeTooltip(pointInfo: DxChartTypes.PointInfo) { const items = pointInfo.valueText?.split('\n'); const color = pointInfo.point?.getColor(); - items?.forEach((item, index) => { + items?.forEach((item: string, index: number) => { if (item.indexOf(pointInfo.seriesName) === 0) { const element = document.createElement('span'); diff --git a/apps/demos/Demos/Diagram/ItemSelection/Vue/App.vue b/apps/demos/Demos/Diagram/ItemSelection/Vue/App.vue index 2f9587c008ef..0c271828b571 100644 --- a/apps/demos/Demos/Diagram/ItemSelection/Vue/App.vue +++ b/apps/demos/Demos/Diagram/ItemSelection/Vue/App.vue @@ -46,7 +46,7 @@ const textExpression = 'Full_Name'; function onContentReady(e: DxDiagramTypes.ContentReadyEvent) { const diagram = e.component; // preselect some shape - const items = diagram.getItems().filter(({ itemType, dataItem }) => itemType === 'shape' && (dataItem[textExpression] === 'Greta Sims')); + const items = diagram.getItems().filter(({ itemType, dataItem }: DxDiagramTypes.Item) => itemType === 'shape' && (dataItem[textExpression] === 'Greta Sims')); if (items.length > 0) { diagram.setSelectedItems(items); diagram.scrollToItem(items[0]); @@ -56,7 +56,7 @@ function onContentReady(e: DxDiagramTypes.ContentReadyEvent) { function onSelectionChanged({ items }: DxDiagramTypes.SelectionChangedEvent) { selectedItemNames.value = 'Nobody has been selected'; const filteredItems = items - .filter((item) => item.itemType === 'shape') + .filter((item: DxDiagramTypes.Item) => item.itemType === 'shape') .map(({ text }: Record) => text); if (filteredItems.length > 0) { selectedItemNames.value = filteredItems.join(', '); diff --git a/apps/demos/Demos/TreeView/DragAndDropHierarchicalDataStructure/Vue/App.vue b/apps/demos/Demos/TreeView/DragAndDropHierarchicalDataStructure/Vue/App.vue index d76877e5223c..57c1c3ab6a62 100644 --- a/apps/demos/Demos/TreeView/DragAndDropHierarchicalDataStructure/Vue/App.vue +++ b/apps/demos/Demos/TreeView/DragAndDropHierarchicalDataStructure/Vue/App.vue @@ -167,7 +167,7 @@ function moveNode( ) { const fromNodeContainingArray = getNodeContainingArray(fromNode, fromItems); const fromIndex = fromNodeContainingArray - ?.findIndex((item) => item.id === fromNode.itemData?.id) || -1; + ?.findIndex((item: DriveItem) => item.id === fromNode.itemData?.id) || -1; if (fromIndex !== -1 && fromNodeContainingArray) { fromNodeContainingArray.splice(fromIndex, 1); @@ -179,7 +179,7 @@ function moveNode( const toNodeContainingArray = getNodeContainingArray(toNode, toItems); const toIndex = toNode === null ? toNodeContainingArray?.length || 0 - : toNodeContainingArray?.findIndex((item) => item.id === toNode.itemData?.id) || 0; + : toNodeContainingArray?.findIndex((item: DriveItem) => item.id === toNode.itemData?.id) || 0; toNodeContainingArray?.splice(toIndex, 0, fromNode.itemData); } } diff --git a/apps/demos/Demos/TreeView/DragAndDropPlainDataStructure/Vue/App.vue b/apps/demos/Demos/TreeView/DragAndDropPlainDataStructure/Vue/App.vue index ca750ebcae7c..8e3c4fce7fa1 100644 --- a/apps/demos/Demos/TreeView/DragAndDropPlainDataStructure/Vue/App.vue +++ b/apps/demos/Demos/TreeView/DragAndDropPlainDataStructure/Vue/App.vue @@ -184,7 +184,7 @@ function moveChildren(node: Node, fromDataSource: DriveItem[], toDataSource: any return; } - node.children?.forEach((child) => { + node.children?.forEach((child: Node) => { if (child.itemData?.isDirectory) { moveChildren(child, fromDataSource, toDataSource); } diff --git a/packages/devextreme-angular/karma.test.shim.js b/packages/devextreme-angular/karma.test.shim.js index f3e70816c60e..cde076c0cfd6 100644 --- a/packages/devextreme-angular/karma.test.shim.js +++ b/packages/devextreme-angular/karma.test.shim.js @@ -8,6 +8,6 @@ testing.TestBed.initTestEnvironment( browser.platformBrowserDynamicTesting(), ); -const context = require.context('./tests/dist', true, /^.\/(?!.*\/(ssr-components|hydration)\.spec\.js$).*\.spec\.js$/); +const context = require.context('./tests/dist', true, /^.\/(?!.*\/(ssr-)[^\.]+\.spec\.js$).*\.spec\.js$/); context.keys().map(context); __karma__.start(); diff --git a/packages/devextreme-angular/project.json b/packages/devextreme-angular/project.json index 975a6e471e9c..bfca55ac5b52 100644 --- a/packages/devextreme-angular/project.json +++ b/packages/devextreme-angular/project.json @@ -341,9 +341,7 @@ "options": { "karmaConfig": "karma.conf.js", "environments": [ - "client", - "server", - "hydration" + "client" ] }, "dependsOn": [ @@ -385,42 +383,6 @@ "build:tests" ] }, - "test:components-server": { - "executor": "devextreme-nx-infra-plugin:karma-multi-env", - "options": { - "karmaConfig": "karma.conf.js", - "environments": [ - "server", - "hydration" - ] - }, - "dependsOn": [ - "build:tests" - ], - "inputs": [ - "default", - "test", - "{projectRoot}/tests/dist/**/*" - ] - }, - "test:components-server-debug": { - "executor": "devextreme-nx-infra-plugin:karma-multi-env", - "options": { - "karmaConfig": "karma.conf.js", - "environments": [ - "server" - ], - "debug": true - }, - "dependsOn": [ - "build:tests" - ], - "inputs": [ - "default", - "test", - "{projectRoot}/tests/dist/**/*" - ] - }, "watch:test": { "executor": "devextreme-nx-infra-plugin:karma-multi-env", "options": { @@ -444,9 +406,7 @@ "options": { "karmaConfig": "karma.conf.js", "environments": [ - "client", - "server", - "hydration" + "client" ] }, "dependsOn": [ @@ -489,8 +449,6 @@ "{projectRoot}/tests/**/*", "{projectRoot}/karma.conf.js", "{projectRoot}/karma.test.shim.js", - "{projectRoot}/karma.server.test.shim.js", - "{projectRoot}/karma.hydration.test.shim.js", "{projectRoot}/webpack.test.js", "{projectRoot}/tsconfig.tests.json", "{projectRoot}/src/**/*.spec.ts", diff --git a/packages/devextreme-angular/tests/src/server/hydration.spec.ts b/packages/devextreme-angular/tests/src/server/hydration.spec.ts deleted file mode 100644 index cc69b14e7a49..000000000000 --- a/packages/devextreme-angular/tests/src/server/hydration.spec.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { BrowserModule, provideClientHydration } from '@angular/platform-browser'; -import { - Component, - destroyPlatform, - NgModule, - PLATFORM_ID, - importProvidersFrom, -} from '@angular/core'; -import { ServerModule, renderModule } from '@angular/platform-server'; -import { DxServerModule } from 'devextreme-angular/server'; -import infernoRenderer from 'devextreme/core/inferno_renderer'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { DevExtremeModule } from 'devextreme-angular'; -import { componentNames as componentNamesAll } from './component-names'; - -const componentNames = componentNamesAll.filter((n) => ['toast', 'action-sheet'].includes(n)); - -const containerClass = 'container'; -const containerSelector = `.${containerClass}`; - -@Component({ - selector: 'app-root', - standalone: false, - template: `
- ${componentNames.map((name) => ``).join('\n')} -
`, -}) -class AppComponent {} - -@NgModule({ - declarations: [AppComponent], - imports: [BrowserModule, DevExtremeModule], - bootstrap: [AppComponent], - providers: [provideClientHydration()], -}) -class AppBrowserModule {} - -@NgModule({ - declarations: [AppComponent], - imports: [ServerModule, DevExtremeModule], - bootstrap: [AppComponent], - providers: [ - provideClientHydration(), - { provide: PLATFORM_ID, useValue: 'server' }, - importProvidersFrom(DxServerModule), - ], -}) -class AppSSRModule {} - -class TestHelpers { - static normalizeClassNames(element: HTMLElement): void { - const classNames = Array.from(element.classList).sort(); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - element.classList.remove(...element.classList); - element.classList.add(...classNames); - } - - static hasConsoleMessage(spy: jasmine.Spy, messages: string[]): boolean { - return spy.calls.allArgs().some((args) => messages.some((msg) => args[0].toLowerCase().includes(msg.toLowerCase()))); - } -} - -describe('Angular Components Hydration Test', () => { - let consoleSpies: { - warn: jasmine.Spy; - error: jasmine.Spy; - log: jasmine.Spy; - }; - const ssrState: { - containerHtml: string; - ssrHTML: string; - } = { - containerHtml: '', - ssrHTML: '', - }; - - beforeAll(() => { - consoleSpies = { - warn: spyOn(console, 'warn').and.callThrough(), - error: spyOn(console, 'error').and.callThrough(), - log: spyOn(console, 'log').and.callThrough(), - }; - }); - - beforeEach(() => { - destroyPlatform(); - }); - - afterEach(() => { - expect(consoleSpies.error).not.toHaveBeenCalled(); - expect(TestHelpers.hasConsoleMessage(consoleSpies.warn, ['exception', 'hydration'])).toBeFalsy(); - }); - - it('should generate correct SSR HTML', async () => { - const html = await renderModule(AppSSRModule, { - document: '', - url: '/', - }); - - ssrState.ssrHTML = html - .replace(/ng-server-context="other"/g, 'ng-server-context="ssg"') - .replace(/^.*.*$/, ''); - - const tempDiv = document.createElement('div'); - tempDiv.innerHTML = ssrState.ssrHTML; - - // Assert - ssrState.containerHtml = tempDiv.querySelector(`${containerSelector}`)?.innerHTML ?? ''; - - expect(ssrState.containerHtml).toBeTruthy(); - expect(ssrState.ssrHTML).toBeTruthy(); - }); - - it('should correctly hydrate server-rendered HTML', async () => { - infernoRenderer.resetInjection(); - - document.body.outerHTML = ssrState.ssrHTML; - - // Act - await platformBrowserDynamic().bootstrapModule(AppBrowserModule); - - expect(TestHelpers.hasConsoleMessage( - consoleSpies.log, - ['Angular hydrated 1 component(s)'], - )).toBeTruthy(); - - expect(ssrState.containerHtml).toEqual(document.querySelector(`${containerSelector}`).innerHTML); - }); -}); diff --git a/packages/devextreme/build/gulp/babel-plugin-add-import-extensions.js b/packages/devextreme/build/gulp/babel-plugin-add-import-extensions.js new file mode 100644 index 000000000000..5b3498895123 --- /dev/null +++ b/packages/devextreme/build/gulp/babel-plugin-add-import-extensions.js @@ -0,0 +1,69 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); + +module.exports = function addImportExtensions() { + return { + name: 'add-import-extensions', + visitor: { + 'ImportDeclaration|ExportNamedDeclaration|ExportAllDeclaration'(astPath) { + const source = astPath.node.source; + + if (!source) return; + + const value = source.value; + + if (!value || (!value.startsWith('./') && !value.startsWith('../'))) { + return; + } + + if (value.match(/\.(js|mjs|json|css)$/)) { + return; + } + + if (value.endsWith('/')) { + source.value = value + 'index.js'; + return; + } + + const currentFile = astPath.hub?.file?.opts?.filename; + const distPathRegExp = /artifacts[\/\\]dist_ts/; + + if (currentFile) { + const currentDir = path.dirname(currentFile); + const resolvedPath = path.resolve(currentDir, value).replace(distPathRegExp,'js'); + + if (fs.existsSync(resolvedPath)) { + const stat = fs.statSync(resolvedPath); + + if (stat.isDirectory()) { + const indexPath = path.join(resolvedPath, 'index.js'); + + if (fs.existsSync(indexPath)) { + source.value = value + '/index.js'; + return; + } + } + } + + let jsFilePath = resolvedPath + '.js'; + + if ( fs.existsSync(jsFilePath) + || fs.existsSync(jsFilePath = resolvedPath + '.ts') + ) { + const stat = fs.statSync(jsFilePath); + + if (stat.isFile()) { + source.value = value + '.js'; + return; + } + } + } + + source.value = value + '.js'; + } + } + }; +}; + diff --git a/packages/devextreme/build/gulp/fix-imports-path.js b/packages/devextreme/build/gulp/fix-imports-path.js new file mode 100644 index 000000000000..b0e55927b4cc --- /dev/null +++ b/packages/devextreme/build/gulp/fix-imports-path.js @@ -0,0 +1,81 @@ +// fix-imports.js +const fs = require('fs'); +const path = require('path'); + +const ROOT_DIR = process.argv[2]; // Папка передаётся первым аргументом + +if (!ROOT_DIR) { + console.error('Использование: node fix-imports.js '); + process.exit(1); +} + +function walkDir(dir, callback) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + walkDir(fullPath, callback); + } else if (entry.isFile() && fullPath.endsWith('.js')) { + callback(fullPath); + } + } +} + +function resolveImport(fileDir, importSpecifier) { + const basePath = path.resolve(fileDir, importSpecifier); + + const candidates = [ + basePath + '.js', + path.join(basePath, 'index.js'), + ]; + + for (const full of candidates) { + if (fs.existsSync(full) && fs.statSync(full).isFile()) { + let rel = path.relative(fileDir, full).replace(/\\/g, '/'); + if (!rel.startsWith('.')) { + rel = './' + rel; + } + return rel; + } + } + return null; +} + +function processFile(filePath) { + const original = fs.readFileSync(filePath, 'utf8'); + let content = original; + const fileDir = path.dirname(filePath); + + const importExportRegex = + /(?:import|export)\s+(?:[^'"]*?\s+from\s+)?(['"])(\.{1,2}\/[^'"]*)\1/g; + + const requireRegex = + /require\(\s*(['"])(\.{1,2}\/[^'"]*)\1\s*\)/g; + + function replaceCallback(_, quote, spec) { + // Если уже есть .js или .mjs — не трогаем + if (spec.endsWith('.js') || spec.endsWith('.mjs')) { + return _; + } + + const resolved = resolveImport(fileDir, spec); + if (!resolved) return _; + + return _.replace(spec, resolved); + } + + content = content.replace(importExportRegex, replaceCallback); + content = content.replace(requireRegex, replaceCallback); + + if (content !== original) { + fs.writeFileSync(filePath, content, 'utf8'); + console.log('Updated:', filePath); + } +} + +; + +module.exports = { + addExtensionToImportsPath: function (dir) { + walkDir(path.resolve(dir), processFile) + }, +}; diff --git a/packages/devextreme/build/gulp/modules_metadata.json b/packages/devextreme/build/gulp/modules_metadata.json index 6ec06d899a89..6c204b5ec4be 100644 --- a/packages/devextreme/build/gulp/modules_metadata.json +++ b/packages/devextreme/build/gulp/modules_metadata.json @@ -654,7 +654,8 @@ "name": "ui/widget/template", "exports": { "Template": { "path": "ui.template", "exportAs": "type" } - } + }, + "types": "./ui/widget/template.d.ts" }, { "name": "utils", @@ -687,6 +688,11 @@ "default": { "path": "viz.dxCircularGauge", "isWidget": true } } }, + { + "name": "viz/common", + "exports": {}, + "types": "./viz/common.d.ts" + }, { "name": "viz/export", "exports": { diff --git a/packages/devextreme/build/gulp/npm.js b/packages/devextreme/build/gulp/npm.js index 9f7f9d5e67bd..c752ce660eb8 100644 --- a/packages/devextreme/build/gulp/npm.js +++ b/packages/devextreme/build/gulp/npm.js @@ -11,6 +11,7 @@ const replace = require('gulp-replace'); const lazyPipe = require('lazypipe'); const gulpFilter = require('gulp-filter'); const gulpRename = require('gulp-rename'); +const MODULES = require('./modules_metadata.json'); const compressionPipes = require('./compression-pipes.js'); const ctx = require('./context.js'); @@ -18,6 +19,8 @@ const env = require('./env-variables.js'); const dataUri = require('./gulp-data-uri').gulpPipe; const headerPipes = require('./header-pipes.js'); const { packageDir, packageDistDir, isEsmPackage, stringSrc, devextremeDistDir } = require('./utils'); +const path = require('path'); +const fs = require('fs'); const resultPath = ctx.RESULT_NPM_PATH; @@ -150,6 +153,58 @@ const sources = (src, dist, distGlob) => (() => merge( const packagePath = `${resultPath}/${packageDir}`; const distPath = `${resultPath}/${packageDistDir}`; +function collectExports(baseDir) { + const exportsMap = {}; + + function getPath(p) { + return path.posix.join(p.replace(/\\/g, '/')) + .replace(/^.+\/esm\//, './esm/') + .replace(/^.+\/cjs\//, './cjs/') + } + + function walk(currentDir, relativePath = '.') { + const packageJsonPath = path.join(currentDir, 'package.json'); + + if (fs.existsSync(packageJsonPath) && !/(cjs|esm)$/.test(currentDir)) { + try { + const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); + const exportEntry = {}; + + if (pkg.module) { + exportEntry.import = getPath(pkg.module); + } + if (pkg.main) { + exportEntry.require = getPath(pkg.main); + } + if (pkg.typings || pkg.types) { + const typesFile = pkg.typings || pkg.types; + exportEntry.types = path.join(currentDir, typesFile) + .replace(/\\/g, '/') + .replace(/^.*\/devextreme\//, './'); + } + + if (Object.keys(exportEntry).length > 0) { + const exportKey = relativePath === '.' ? '.' : `./${relativePath.replace(/\\/g, '/')}`; + exportsMap[exportKey] = exportEntry; + } + } catch (err) { + console.warn(`Failed to read package.json in ${packageJsonPath}:`, err.message); + } + } + + const entries = fs.readdirSync(currentDir, { withFileTypes: true }); + + for (const entry of entries) { + if (entry.isDirectory()) { + walk(path.join(currentDir, entry.name), path.join(relativePath, entry.name)); + } + } + } + + walk(baseDir); + return exportsMap; +} + gulp.task('npm-sources', gulp.series( 'ts-sources', () => gulp @@ -164,6 +219,33 @@ gulp.task('npm-dist', () => gulp .pipe(gulp.dest(distPath)) ); +gulp.task('add-exports-to-package-json', () => gulp + .src(`${packagePath}/package.json`) + .pipe( + through.obj((file, enc, callback) => { + const pkg = JSON.parse(file.contents.toString(enc)); + + pkg.exports = { + "./dist/*":"./dist/*", + ...collectExports(path.resolve(packagePath)) + }; + + MODULES.forEach((item) => { + const exportPath = './' + item.name; + if(item.types && !pkg.exports[exportPath]?.types) { + pkg.exports[exportPath] = pkg.exports[exportPath] || {}; + pkg.exports[exportPath].types = item.types; + } + }) + + file.contents = Buffer.from(JSON.stringify(pkg, null, 2)); + + callback(null, file); + }) + ) + .pipe(gulp.dest(packagePath)) +); + const scssDir = `${packagePath}/scss`; gulp.task('npm-sass', gulp.series( @@ -183,4 +265,5 @@ gulp.task('npm-sass', gulp.series( ) )); -gulp.task('npm', gulp.series('npm-sources', 'npm-dist', 'ts-check-public-modules', 'npm-sass')); +gulp.task('npm', gulp.series('npm-sources', 'npm-dist', 'ts-check-public-modules', 'npm-sass', 'add-exports-to-package-json')); + diff --git a/packages/devextreme/build/gulp/transpile-config.js b/packages/devextreme/build/gulp/transpile-config.js index 9bee0fbd6141..efd7e9aafdd4 100644 --- a/packages/devextreme/build/gulp/transpile-config.js +++ b/packages/devextreme/build/gulp/transpile-config.js @@ -1,5 +1,7 @@ 'use strict'; +const addImportExtensions = require('./babel-plugin-add-import-extensions'); + const common = { plugins: [ ['babel-plugin-inferno', { 'imports': true }], @@ -33,11 +35,12 @@ module.exports = { esm: Object.assign({}, common, { // eslint-disable-next-line spellcheck/spell-checker presets: [['@babel/preset-env', { targets, modules: false }]], - plugins: common.plugins.concat( - [['@babel/plugin-transform-runtime', { + plugins: common.plugins.concat([ + addImportExtensions, + ['@babel/plugin-transform-runtime', { useESModules: true, version: '7.5.0' // https://github.com/babel/babel/issues/10261#issuecomment-514687857 - }]] - ) + }] + ]) }) }; diff --git a/packages/devextreme/js/__internal/core/state_manager/dev/redux_dev_tools_connector.ts b/packages/devextreme/js/__internal/core/state_manager/dev/redux_dev_tools_connector.ts index a949ad99a00d..d347aa3b72fe 100644 --- a/packages/devextreme/js/__internal/core/state_manager/dev/redux_dev_tools_connector.ts +++ b/packages/devextreme/js/__internal/core/state_manager/dev/redux_dev_tools_connector.ts @@ -1,8 +1,11 @@ /* eslint-disable spellcheck/spell-checker */ +import { getWindow } from '../../utils/m_window'; import { EventEmitter } from './event_emitter'; import type * as StateManagementTypes from './types'; import { isObject } from './utils'; +const window = getWindow(); + export class ReduxDevToolsConnector implements StateManagementTypes.DevToolsConnector { private devTools: StateManagementTypes.ReduxDevToolsInstance | null = null; diff --git a/packages/devextreme/js/__internal/scheduler/recurrence/generate_dates.ts b/packages/devextreme/js/__internal/scheduler/recurrence/generate_dates.ts index 6027f03aa3ba..b0292084a32f 100644 --- a/packages/devextreme/js/__internal/scheduler/recurrence/generate_dates.ts +++ b/packages/devextreme/js/__internal/scheduler/recurrence/generate_dates.ts @@ -1,11 +1,14 @@ import { dateUtilsTs } from '@ts/core/utils/date'; -import { RRule, RRuleSet } from 'rrule'; +import type { RRuleSet as RRuleSetType } from 'rrule'; +import * as rrule from 'rrule'; import timeZoneUtils from '../m_utils_time_zone'; import { getDateByAsciiString, parseRecurrenceRule } from './base'; import type { ProcessorOptions, RRuleParams } from './types'; import { validateRRuleObject } from './validate_rule'; +const { RRule, RRuleSet } = rrule; + const { addOffsets } = dateUtilsTs; const MS_IN_HOUR = 1000 * 60 * 60; @@ -121,7 +124,7 @@ const createRRule = ( options: ProcessorOptions, startDateUtc: Date, until?: Date | null, -): RRuleSet => { +): RRuleSetType => { const ruleOptions = RRule.parseString(String(options.rule)); const { firstDayOfWeek } = options; diff --git a/packages/devextreme/js/__internal/scheduler/view_model/filtration/utils/split_by_recurrence/generate_recurrence_utc_dates.ts b/packages/devextreme/js/__internal/scheduler/view_model/filtration/utils/split_by_recurrence/generate_recurrence_utc_dates.ts index e2587cf09bd5..400fa0c74e1d 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/filtration/utils/split_by_recurrence/generate_recurrence_utc_dates.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/filtration/utils/split_by_recurrence/generate_recurrence_utc_dates.ts @@ -1,10 +1,12 @@ import { dateUtilsTs } from '@ts/core/utils/date'; -import { RRule, RRuleSet } from 'rrule'; +import * as rrule from 'rrule'; import { parseRecurrenceRule } from '../../../../recurrence/base'; import type { DateInterval, MinimalAppointmentEntity } from '../../../types'; import { getDateOffsetMs } from './get_date_information'; +const { RRule, RRuleSet } = rrule; + interface Options { firstDayOfWeek?: number; interval: DateInterval;