Skip to content

Commit d09352e

Browse files
alexeyr-ci2alexeyrRomex91
authored
Switch to ESM (#1726)
* Add Jest tests for importing/requiring react-on-rails * Update attw * Ship ESM build * Test type: module * Add extensions to imports * Remove module/moduleResolution again * Fix ReactDOM build errors * Move require into .cts * Fix Jest config * Add changelog and release notes * Import ReactDOMServer from reactApis instead of directly * Improve react-dom handling * Optimize react-dom/server bundle size * Move react-dom/server reexports to a separate file * Switch to fully undeprecated ts-jest config * Deduplicate spec/dummy dependencies --------- Co-authored-by: Alexey Romanov <alexey.v.romanov@gmail.com> Co-authored-by: Roman Kuksin <roman@shakacode.com>
1 parent db43f02 commit d09352e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1773
-2007
lines changed

.github/workflows/lint-js-and-ruby.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,8 @@ jobs:
8989
- name: Pack for attw and publint
9090
run: yarn pack -f react-on-rails.tgz
9191
- name: Lint package types
92-
# --profile because we don't care about node10
93-
# --ignore-rules CJS default export can't be resolved at the moment,
94-
# revisit in 15.0.0
95-
run: yarn run attw react-on-rails.tgz --profile node16 --ignore-rules cjs-only-exports-default
92+
# our package is ESM-only
93+
run: yarn run attw react-on-rails.tgz --profile esm-only
9694
- name: Lint package publishing
9795
run: yarn run publint --strict react-on-rails.tgz
9896
# We only download and run Actionlint if there is any difference in GitHub Action workflows

.github/workflows/main.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ jobs:
154154
run: cd spec/dummy && yalc add react-on-rails
155155
- name: Install Node modules with Yarn for dummy app
156156
run: cd spec/dummy && yarn install --no-progress --no-emoji
157+
- name: Dummy JS tests
158+
run: |
159+
cd spec/dummy
160+
yarn run test:js
157161
- name: Install Ruby Gems for package
158162
run: |
159163
bundle lock --add-platform 'x86_64-linux'

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Changes since the last non-beta release.
3535

3636
### Changed
3737

38+
- **Breaking change**: The package is ESM-only now. Please see [Release Notes](docs/release-notes/15.0.0.md#esm-only-package) for more details.
3839
- The global context is now accessed using `globalThis`. [PR 1727](https://github.com/shakacode/react_on_rails/pull/1727) by [alexeyr-ci2](https://github.com/alexeyr-ci2).
3940
- Generated client packs now import from `react-on-rails/client` instead of `react-on-rails`. [PR 1706](https://github.com/shakacode/react_on_rails/pull/1706) by [alexeyr-ci](https://github.com/alexeyr-ci).
4041
- The "optimization opportunity" message when importing the server-side `react-on-rails` instead of `react-on-rails/client` in browsers is now a warning for two reasons:

docs/release-notes/15.0.0.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,15 @@ Major improvements to component and store hydration:
6868
- If you were previously using `defer_generated_component_packs: false`, use `generated_component_packs_loading_strategy: :sync` instead
6969
- For optimal performance with Shakapacker ≥ 8.2.0, consider using `generated_component_packs_loading_strategy: :async`
7070

71+
### ESM-only package
72+
73+
The package is now published as ES Modules instead of CommonJS. In most cases it shouldn't affect your code, as bundlers will be able to handle it. However:
74+
75+
- If you explicitly use `require('react-on-rails')`, and can't change to `import`, upgrade to Node v20.19.0+ or v22.12.0+. They allow `require` for ESM modules without any flags. Node v20.17.0+ with `--experimental-require-module` should work as well.
76+
- If you run into `TS1479: The current file is a CommonJS module whose imports will produce 'require' calls; however, the referenced file is an ECMAScript module and cannot be imported with 'require'.` TypeScript error, you'll need to [upgrade to TypeScript 5.8 and set `module` to `nodenext`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-8.html#support-for-require-of-ecmascript-modules-in---module-nodenext).
77+
78+
Finally, if everything else fails, please contact us and we'll help you upgrade or release a dual ESM-CJS version.
79+
7180
### `globalThis`
7281

7382
[`globalThis`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis) is now used in code.

eslint.config.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ const config = tsEslint.config([
133133
'jsx-a11y/anchor-is-valid': 'off',
134134
},
135135
},
136+
{
137+
files: ['node_package/**/*'],
138+
rules: {
139+
'import/extensions': ['error', 'ignorePackages'],
140+
},
141+
},
136142
{
137143
files: ['lib/generators/react_on_rails/templates/**/*'],
138144
rules: {
@@ -143,7 +149,7 @@ const config = tsEslint.config([
143149
},
144150
},
145151
{
146-
files: ['**/*.ts', '**/*.tsx'],
152+
files: ['**/*.ts{x,}', '**/*.[cm]ts'],
147153

148154
extends: tsEslint.configs.strictTypeChecked,
149155

jest.config.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
1+
import { createJsWithTsPreset } from 'ts-jest';
2+
13
const nodeVersion = parseInt(process.version.slice(1), 10);
24

3-
module.exports = {
4-
preset: 'ts-jest/presets/js-with-ts',
5+
export default {
6+
...createJsWithTsPreset({
7+
tsconfig: {
8+
// Relative imports in our TS code include `.ts` extensions.
9+
// When compiling the package, TS rewrites them to `.js`,
10+
// but ts-jest runs on the original code where the `.js` files don't exist,
11+
// so this setting needs to be disabled here.
12+
rewriteRelativeImportExtensions: false,
13+
},
14+
}),
515
testEnvironment: 'jsdom',
616
setupFiles: ['<rootDir>/node_package/tests/jest.setup.js'],
717
// React Server Components tests are compatible with React 19

node_package/src/Authenticity.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AuthenticityHeaders } from './types/index';
1+
import type { AuthenticityHeaders } from './types/index.ts';
22

33
export function authenticityToken(): string | null {
44
const token = document.querySelector('meta[name="csrf-token"]');

node_package/src/CallbackRegistry.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { ItemRegistrationCallback } from './types';
2-
import { onPageLoaded, onPageUnloaded } from './pageLifecycle';
3-
import { getRailsContext } from './context';
1+
import { ItemRegistrationCallback } from './types/index.ts';
2+
import { onPageLoaded, onPageUnloaded } from './pageLifecycle.ts';
3+
import { getRailsContext } from './context.ts';
44

55
/**
66
* Represents information about a registered item including its value,

node_package/src/ClientSideRenderer.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
/* eslint-disable max-classes-per-file */
2-
/* eslint-disable react/no-deprecated,@typescript-eslint/no-deprecated -- while we need to support React 16 */
32

4-
import * as ReactDOM from 'react-dom';
53
import type { ReactElement } from 'react';
6-
import type { RailsContext, RegisteredComponent, RenderFunction, Root } from './types';
4+
import type { RailsContext, RegisteredComponent, RenderFunction, Root } from './types/index.ts';
75

8-
import { getRailsContext, resetRailsContext } from './context';
9-
import createReactOutput from './createReactOutput';
10-
import { isServerRenderHash } from './isServerRenderResult';
11-
import reactHydrateOrRender from './reactHydrateOrRender';
12-
import { supportsRootApi } from './reactApis';
13-
import { debugTurbolinks } from './turbolinksUtils';
14-
import * as StoreRegistry from './StoreRegistry';
15-
import * as ComponentRegistry from './ComponentRegistry';
6+
import { getRailsContext, resetRailsContext } from './context.ts';
7+
import createReactOutput from './createReactOutput.ts';
8+
import { isServerRenderHash } from './isServerRenderResult.ts';
9+
import { supportsHydrate, supportsRootApi, unmountComponentAtNode } from './reactApis.cts';
10+
import reactHydrateOrRender from './reactHydrateOrRender.ts';
11+
import { debugTurbolinks } from './turbolinksUtils.ts';
12+
import * as StoreRegistry from './StoreRegistry.ts';
13+
import * as ComponentRegistry from './ComponentRegistry.ts';
1614

1715
const REACT_ON_RAILS_STORE_ATTRIBUTE = 'data-js-react-on-rails-store';
1816

@@ -103,8 +101,7 @@ class ComponentRenderer {
103101
}
104102

105103
// Hydrate if available and was server rendered
106-
// @ts-expect-error potentially present if React 18 or greater
107-
const shouldHydrate = !!(ReactDOM.hydrate || ReactDOM.hydrateRoot) && !!domNode.innerHTML;
104+
const shouldHydrate = supportsHydrate && !!domNode.innerHTML;
108105

109106
const reactElementOrRouterResult = createReactOutput({
110107
componentObj,
@@ -156,7 +153,8 @@ You should return a React.Component always for the client side entry point.`);
156153
}
157154

158155
try {
159-
ReactDOM.unmountComponentAtNode(domNode);
156+
// eslint-disable-next-line @typescript-eslint/no-deprecated
157+
unmountComponentAtNode(domNode);
160158
} catch (e: unknown) {
161159
const error = e instanceof Error ? e : new Error('Unknown error');
162160
console.info(

node_package/src/ComponentRegistry.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { type RegisteredComponent, type ReactComponentOrRenderFunction } from './types';
2-
import isRenderFunction from './isRenderFunction';
3-
import CallbackRegistry from './CallbackRegistry';
1+
import { type RegisteredComponent, type ReactComponentOrRenderFunction } from './types/index.ts';
2+
import isRenderFunction from './isRenderFunction.ts';
3+
import CallbackRegistry from './CallbackRegistry.ts';
44

55
const componentRegistry = new CallbackRegistry<RegisteredComponent>('component');
66

0 commit comments

Comments
 (0)