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 <[email protected]> Co-authored-by: Roman Kuksin <[email protected]>
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

+2-4
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

+4
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

+1
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

+9
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

+7-1
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

+12-2
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

+1-1
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

+3-3
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

+12-14
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

+3-3
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

node_package/src/RSCClientRoot.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
import * as React from 'react';
44
import * as ReactDOMClient from 'react-dom/client';
55
import { createFromReadableStream } from 'react-on-rails-rsc/client';
6-
import { fetch } from './utils';
7-
import transformRSCStreamAndReplayConsoleLogs from './transformRSCStreamAndReplayConsoleLogs';
8-
import { RailsContext, RenderFunction } from './types';
6+
import { fetch } from './utils.ts';
7+
import transformRSCStreamAndReplayConsoleLogs from './transformRSCStreamAndReplayConsoleLogs.ts';
8+
import { RailsContext, RenderFunction } from './types/index.ts';
99

1010
const { use } = React;
1111

node_package/src/ReactDOMServer.cts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Depending on react-dom version, proper ESM import can be react-dom/server or react-dom/server.js
2+
// but since we have a .cts file, it supports both.
3+
// Remove this file and replace by imports directly from 'react-dom/server' when we drop React 16/17 support.
4+
export { renderToPipeableStream, renderToString, type PipeableStream } from 'react-dom/server';

node_package/src/ReactOnRails.client.ts

+10-10
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import type { ReactElement } from 'react';
2-
import * as ClientStartup from './clientStartup';
3-
import { renderOrHydrateComponent, hydrateStore } from './ClientSideRenderer';
4-
import * as ComponentRegistry from './ComponentRegistry';
5-
import * as StoreRegistry from './StoreRegistry';
6-
import buildConsoleReplay from './buildConsoleReplay';
7-
import createReactOutput from './createReactOutput';
8-
import * as Authenticity from './Authenticity';
2+
import * as ClientStartup from './clientStartup.ts';
3+
import { renderOrHydrateComponent, hydrateStore } from './ClientSideRenderer.ts';
4+
import * as ComponentRegistry from './ComponentRegistry.ts';
5+
import * as StoreRegistry from './StoreRegistry.ts';
6+
import buildConsoleReplay from './buildConsoleReplay.ts';
7+
import createReactOutput from './createReactOutput.ts';
8+
import * as Authenticity from './Authenticity.ts';
99
import type {
1010
RegisteredComponent,
1111
RenderResult,
@@ -15,8 +15,8 @@ import type {
1515
Store,
1616
StoreGenerator,
1717
ReactOnRailsOptions,
18-
} from './types';
19-
import reactHydrateOrRender from './reactHydrateOrRender';
18+
} from './types/index.ts';
19+
import reactHydrateOrRender from './reactHydrateOrRender.ts';
2020

2121
if (globalThis.ReactOnRails !== undefined) {
2222
throw new Error(`\
@@ -194,5 +194,5 @@ globalThis.ReactOnRails.resetOptions();
194194

195195
ClientStartup.clientStartup();
196196

197-
export * from './types';
197+
export * from './types/index.ts';
198198
export default globalThis.ReactOnRails;

node_package/src/ReactOnRails.full.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import handleError from './handleError';
2-
import serverRenderReactComponent from './serverRenderReactComponent';
3-
import type { RenderParams, RenderResult, ErrorOptions } from './types';
1+
import handleError from './handleError.ts';
2+
import serverRenderReactComponent from './serverRenderReactComponent.ts';
3+
import type { RenderParams, RenderResult, ErrorOptions } from './types/index.ts';
44

5-
import Client from './ReactOnRails.client';
5+
import Client from './ReactOnRails.client.ts';
66

77
if (typeof window !== 'undefined') {
88
// warn to include a collapsed stack trace
@@ -16,5 +16,5 @@ Client.handleError = (options: ErrorOptions): string | undefined => handleError(
1616
Client.serverRenderReactComponent = (options: RenderParams): null | string | Promise<RenderResult> =>
1717
serverRenderReactComponent(options);
1818

19-
export * from './types';
19+
export * from './types/index.ts';
2020
export default Client;

node_package/src/ReactOnRails.node.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import ReactOnRails from './ReactOnRails.full';
2-
import streamServerRenderedReactComponent from './streamServerRenderedReactComponent';
1+
import ReactOnRails from './ReactOnRails.full.ts';
2+
import streamServerRenderedReactComponent from './streamServerRenderedReactComponent.ts';
33

44
ReactOnRails.streamServerRenderedReactComponent = streamServerRenderedReactComponent;
55

6-
export * from './ReactOnRails.full';
6+
export * from './ReactOnRails.full.ts';
77
// eslint-disable-next-line no-restricted-exports -- see https://github.com/eslint/eslint/issues/15617
8-
export { default } from './ReactOnRails.full';
8+
export { default } from './ReactOnRails.full.ts';

node_package/src/ReactOnRailsRSC.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import { renderToPipeableStream } from 'react-on-rails-rsc/server.node';
22
import { PassThrough, Readable } from 'stream';
33

4-
import { RSCRenderParams, StreamRenderState, StreamableComponentResult } from './types';
5-
import ReactOnRails from './ReactOnRails.full';
6-
import buildConsoleReplay from './buildConsoleReplay';
7-
import handleError from './handleError';
8-
import { convertToError, createResultObject } from './serverRenderUtils';
4+
import { RSCRenderParams, StreamRenderState, StreamableComponentResult } from './types/index.ts';
5+
import ReactOnRails from './ReactOnRails.full.ts';
6+
import buildConsoleReplay from './buildConsoleReplay.ts';
7+
import handleError from './handleError.ts';
8+
import { convertToError, createResultObject } from './serverRenderUtils.ts';
99

1010
import {
1111
streamServerRenderedComponent,
1212
transformRenderStreamChunksToResultObject,
13-
} from './streamServerRenderedReactComponent';
14-
import loadReactClientManifest from './loadReactClientManifest';
13+
} from './streamServerRenderedReactComponent.ts';
14+
import loadReactClientManifest from './loadReactClientManifest.ts';
1515

1616
const stringToStream = (str: string) => {
1717
const stream = new PassThrough();
@@ -67,5 +67,5 @@ ReactOnRails.serverRenderRSCReactComponent = (options: RSCRenderParams) => {
6767
}
6868
};
6969

70-
export * from './types';
70+
export * from './types/index.ts';
7171
export default ReactOnRails;

node_package/src/StoreRegistry.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import CallbackRegistry from './CallbackRegistry';
2-
import type { Store, StoreGenerator } from './types';
1+
import CallbackRegistry from './CallbackRegistry.ts';
2+
import type { Store, StoreGenerator } from './types/index.ts';
33

44
const storeGeneratorRegistry = new CallbackRegistry<StoreGenerator>('store generator');
55
const hydratedStoreRegistry = new CallbackRegistry<Store>('hydrated store');

node_package/src/buildConsoleReplay.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { wrapInScriptTags } from './RenderUtils';
2-
import scriptSanitizedVal from './scriptSanitizedVal';
1+
import { wrapInScriptTags } from './RenderUtils.ts';
2+
import scriptSanitizedVal from './scriptSanitizedVal.ts';
33

44
declare global {
55
interface Console {

node_package/src/clientStartup.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import {
44
renderOrHydrateAllComponents,
55
renderOrHydrateForceLoadedComponents,
66
unmountAll,
7-
} from './ClientSideRenderer';
8-
import { onPageLoaded, onPageUnloaded } from './pageLifecycle';
9-
import { debugTurbolinks } from './turbolinksUtils';
7+
} from './ClientSideRenderer.ts';
8+
import { onPageLoaded, onPageUnloaded } from './pageLifecycle.ts';
9+
import { debugTurbolinks } from './turbolinksUtils.ts';
1010

1111
export async function reactOnRailsPageLoaded() {
1212
debugTurbolinks('reactOnRailsPageLoaded');

node_package/src/context.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ReactOnRailsInternal, RailsContext } from './types';
1+
import type { ReactOnRailsInternal, RailsContext } from './types/index.ts';
22

33
declare global {
44
/* eslint-disable no-var,vars-on-top,no-underscore-dangle */

node_package/src/createReactOutput.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
2-
import type { CreateParams, ReactComponent, RenderFunction, CreateReactOutputResult } from './types/index';
3-
import { isServerRenderHash, isPromise } from './isServerRenderResult';
2+
import type { CreateParams, ReactComponent, RenderFunction, CreateReactOutputResult } from './types/index.ts';
3+
import { isServerRenderHash, isPromise } from './isServerRenderResult.ts';
44

55
function createReactElementFromRenderFunctionResult(
66
renderFunctionResult: ReactComponent,

node_package/src/handleError.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
2-
import * as ReactDOMServer from 'react-dom/server';
3-
import type { ErrorOptions } from './types/index';
2+
import { renderToString } from './ReactDOMServer.cts';
3+
import type { ErrorOptions } from './types/index.ts';
44

55
function handleRenderFunctionIssue(options: ErrorOptions): string {
66
const { e, name } = options;
@@ -60,8 +60,8 @@ Message: ${e.message}
6060
${e.stack}`;
6161

6262
const reactElement = React.createElement('pre', null, msg);
63-
if (typeof ReactDOMServer.renderToString === 'function') {
64-
return ReactDOMServer.renderToString(reactElement);
63+
if (typeof renderToString === 'function') {
64+
return renderToString(reactElement);
6565
}
6666
return msg;
6767
}

node_package/src/isRenderFunction.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// See discussion:
22
// https://discuss.reactjs.org/t/how-to-determine-if-js-object-is-react-component/2825/2
3-
import { ReactComponentOrRenderFunction, RenderFunction } from './types/index';
3+
import { ReactComponentOrRenderFunction, RenderFunction } from './types/index.ts';
44

55
/**
66
* Used to determine we'll call be calling React.createElement on the component of if this is a

node_package/src/isServerRenderResult.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type {
33
ServerRenderResult,
44
RenderFunctionResult,
55
RenderStateHtml,
6-
} from './types/index';
6+
} from './types/index.ts';
77

88
export function isServerRenderHash(
99
testValue: CreateReactOutputResult | RenderFunctionResult,

node_package/src/pageLifecycle.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
turbolinksSupported,
55
turboInstalled,
66
turbolinksVersion5,
7-
} from './turbolinksUtils';
7+
} from './turbolinksUtils.ts';
88

99
type PageLifecycleCallback = () => void | Promise<void>;
1010
type PageState = 'load' | 'unload' | 'initial';

0 commit comments

Comments
 (0)