Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/lint-js-and-ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,7 @@ jobs:
run: yarn pack -f react-on-rails.tgz
- name: Lint package types
# --profile because we don't care about node10
# --ignore-rules CJS default export can't be resolved at the moment,
# revisit in 15.0.0
run: yarn run attw react-on-rails.tgz --profile node16 --ignore-rules cjs-only-exports-default
run: yarn run attw react-on-rails.tgz --profile node16
- name: Lint package publishing
run: yarn run publint --strict react-on-rails.tgz
# We only download and run Actionlint if there is any difference in GitHub Action workflows
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Changes since the last non-beta release.
#### Added

- Configuration option `generated_component_packs_loading_strategy` to control how generated component packs are loaded. It supports `sync`, `async`, and `defer` strategies. [PR 1712](https://github.com/shakacode/react_on_rails/pull/1712) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
- The package now includes both ESM and CJS versions. [PR 1722](https://github.com/shakacode/react_on_rails/pull/1722) by [alexeyr-ci2](https://github.com/alexeyr-ci2).

### Removed (Breaking Changes)

Expand Down
8 changes: 8 additions & 0 deletions docs/release-notes/15.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ Major improvements to component and store hydration:
- `:sync` - Loads scripts synchronously (default for Shakapacker < 8.2.0) (better to upgrade to Shakapacker 8.2.0 and use `:async` strategy)
- Improves page performance by optimizing how component packs are loaded

### ESM Support

The package is now published in both ES Module and CommonJS formats.
In the next major release we'll work on making it much more tree-shakable to reduce bundle size.
In most cases you don't need to do anything to take advantage of that,
but if you use `require('react-on-rails')` (or `'react-on-rails/...'`),
consider replacing it by `import`.

## Breaking Changes

### Component Hydration Changes
Expand Down
8 changes: 7 additions & 1 deletion eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@ const config = tsEslint.config([
'jsx-a11y/anchor-is-valid': 'off',
},
},
{
files: ['node_package/src/**/*'],
rules: {
'import/extensions': ['error', 'ignorePackages'],
},
},
{
files: ['lib/generators/react_on_rails/templates/**/*'],
rules: {
Expand All @@ -138,7 +144,7 @@ const config = tsEslint.config([
},
},
{
files: ['**/*.ts', '**/*.tsx'],
files: ['**/*.ts', '**/*.[cm]ts', '**/*.tsx'],

extends: tsEslint.configs.strictTypeChecked,

Expand Down
5 changes: 5 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ const nodeVersion = parseInt(process.version.slice(1), 10);

module.exports = {
preset: 'ts-jest/presets/js-with-ts',
globals: {
'ts-jest': {
tsconfig: 'tsconfig.jest.json',
},
},
testEnvironment: 'jsdom',
setupFiles: ['<rootDir>/node_package/tests/jest.setup.js'],
// React Server Components tests are compatible with React 19
Expand Down
9 changes: 9 additions & 0 deletions node_package/scripts/build
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/sh

set -e

yarn run clean
yarn run tsc --declaration
echo '{ "type": "module" }' > node_package/lib/esm/package.json
yarn run tsc -p tsconfig.cjs.json --declaration
echo '{ "type": "commonjs" }' > node_package/lib/cjs/package.json
2 changes: 1 addition & 1 deletion node_package/src/Authenticity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AuthenticityHeaders } from './types/index';
import type { AuthenticityHeaders } from './_types.ts';

export default {
authenticityToken(): string | null {
Expand Down
6 changes: 3 additions & 3 deletions node_package/src/CallbackRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ItemRegistrationCallback } from './types';
import { onPageLoaded, onPageUnloaded } from './pageLifecycle';
import { getContextAndRailsContext } from './context';
import { ItemRegistrationCallback } from './_types.ts';
import { onPageLoaded, onPageUnloaded } from './pageLifecycle.ts';
import { getContextAndRailsContext } from './context.ts';

/**
* Represents information about a registered item including its value,
Expand Down
22 changes: 10 additions & 12 deletions node_package/src/ClientSideRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
/* eslint-disable max-classes-per-file */
/* eslint-disable react/no-deprecated,@typescript-eslint/no-deprecated -- while we need to support React 16 */
/* eslint-disable @typescript-eslint/no-deprecated -- while we need to support React 16 */

import * as ReactDOM from 'react-dom';
import type { ReactElement } from 'react';
import type { RailsContext, RegisteredComponent, RenderFunction, Root } from './types';
import type { RailsContext, RegisteredComponent, RenderFunction, Root } from './_types.ts';

import { getContextAndRailsContext, resetContextAndRailsContext, type Context } from './context';
import createReactOutput from './createReactOutput';
import { isServerRenderHash } from './isServerRenderResult';
import reactHydrateOrRender from './reactHydrateOrRender';
import { supportsRootApi } from './reactApis';
import { debugTurbolinks } from './turbolinksUtils';
import { getContextAndRailsContext, resetContextAndRailsContext, type Context } from './context.ts';
import createReactOutput from './createReactOutput.ts';
import { isServerRenderHash } from './isServerRenderResult.ts';
import reactHydrateOrRender from './reactHydrateOrRender.cts';
import { canHydrate, unmountComponentAtNode, supportsRootApi } from './reactApis.cts';
import { debugTurbolinks } from './turbolinksUtils.ts';

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

Expand Down Expand Up @@ -101,8 +100,7 @@ class ComponentRenderer {
}

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

const reactElementOrRouterResult = createReactOutput({
componentObj,
Expand Down Expand Up @@ -154,7 +152,7 @@ You should return a React.Component always for the client side entry point.`);
}

try {
ReactDOM.unmountComponentAtNode(domNode);
unmountComponentAtNode(domNode);
} catch (e: unknown) {
const error = e instanceof Error ? e : new Error('Unknown error');
console.info(
Expand Down
6 changes: 3 additions & 3 deletions node_package/src/ComponentRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type RegisteredComponent, type ReactComponentOrRenderFunction } from './types';
import isRenderFunction from './isRenderFunction';
import CallbackRegistry from './CallbackRegistry';
import { type RegisteredComponent, type ReactComponentOrRenderFunction } from './_types.ts';
import isRenderFunction from './isRenderFunction.ts';
import CallbackRegistry from './CallbackRegistry.ts';

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

Expand Down
6 changes: 3 additions & 3 deletions node_package/src/RSCClientRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import * as React from 'react';
import * as ReactDOMClient from 'react-dom/client';
import { createFromReadableStream } from 'react-on-rails-rsc/client';
import { fetch } from './utils';
import transformRSCStreamAndReplayConsoleLogs from './transformRSCStreamAndReplayConsoleLogs';
import { RailsContext, RenderFunction } from './types';
import { fetch } from './utils.ts';
import transformRSCStreamAndReplayConsoleLogs from './transformRSCStreamAndReplayConsoleLogs.ts';
import { RailsContext, RenderFunction } from './_types.ts';

const { use } = React;

Expand Down
22 changes: 11 additions & 11 deletions node_package/src/ReactOnRails.client.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { ReactElement } from 'react';
import * as ClientStartup from './clientStartup';
import { renderOrHydrateComponent, hydrateStore } from './ClientSideRenderer';
import ComponentRegistry from './ComponentRegistry';
import StoreRegistry from './StoreRegistry';
import buildConsoleReplay from './buildConsoleReplay';
import createReactOutput from './createReactOutput';
import Authenticity from './Authenticity';
import context from './context';
import * as ClientStartup from './clientStartup.ts';
import { renderOrHydrateComponent, hydrateStore } from './ClientSideRenderer.ts';
import ComponentRegistry from './ComponentRegistry.ts';
import StoreRegistry from './StoreRegistry.ts';
import buildConsoleReplay from './buildConsoleReplay.ts';
import createReactOutput from './createReactOutput.ts';
import Authenticity from './Authenticity.ts';
import context from './context.ts';
import type {
RegisteredComponent,
RenderResult,
Expand All @@ -16,8 +16,8 @@ import type {
Store,
StoreGenerator,
ReactOnRailsOptions,
} from './types';
import reactHydrateOrRender from './reactHydrateOrRender';
} from './_types.ts';
import reactHydrateOrRender from './reactHydrateOrRender.cts';

const ctx = context();

Expand Down Expand Up @@ -203,5 +203,5 @@ ctx.ReactOnRails.resetOptions();

ClientStartup.clientStartup(ctx);

export * from './types';
export * from './_types.ts';
export default ctx.ReactOnRails;
10 changes: 5 additions & 5 deletions node_package/src/ReactOnRails.full.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import handleError from './handleError';
import serverRenderReactComponent from './serverRenderReactComponent';
import type { RenderParams, RenderResult, ErrorOptions } from './types';
import handleError from './handleError.ts';
import serverRenderReactComponent from './serverRenderReactComponent.ts';
import type { RenderParams, RenderResult, ErrorOptions } from './_types.ts';

import Client from './ReactOnRails.client';
import Client from './ReactOnRails.client.ts';

if (typeof window !== 'undefined') {
console.log(
Expand All @@ -15,5 +15,5 @@ Client.handleError = (options: ErrorOptions): string | undefined => handleError(
Client.serverRenderReactComponent = (options: RenderParams): null | string | Promise<RenderResult> =>
serverRenderReactComponent(options);

export * from './types';
export * from './_types.ts';
export default Client;
8 changes: 4 additions & 4 deletions node_package/src/ReactOnRails.node.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import ReactOnRails from './ReactOnRails.full';
import streamServerRenderedReactComponent from './streamServerRenderedReactComponent';
import ReactOnRails from './ReactOnRails.full.ts';
import streamServerRenderedReactComponent from './streamServerRenderedReactComponent.ts';

ReactOnRails.streamServerRenderedReactComponent = streamServerRenderedReactComponent;

export * from './ReactOnRails.full';
export * from './ReactOnRails.full.ts';
// eslint-disable-next-line no-restricted-exports -- see https://github.com/eslint/eslint/issues/15617
export { default } from './ReactOnRails.full';
export { default } from './ReactOnRails.full.ts';
16 changes: 8 additions & 8 deletions node_package/src/ReactOnRailsRSC.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ import { renderToPipeableStream } from 'react-on-rails-rsc/server.node';
import { PassThrough, Readable } from 'stream';
import type { ReactElement } from 'react';

import { RSCRenderParams, StreamRenderState } from './types';
import ReactOnRails from './ReactOnRails.full';
import buildConsoleReplay from './buildConsoleReplay';
import handleError from './handleError';
import { convertToError, createResultObject } from './serverRenderUtils';
import { RSCRenderParams, StreamRenderState } from './_types.ts';
import ReactOnRails from './ReactOnRails.full.ts';
import buildConsoleReplay from './buildConsoleReplay.ts';
import handleError from './handleError.ts';
import { convertToError, createResultObject } from './serverRenderUtils.ts';

import {
streamServerRenderedComponent,
transformRenderStreamChunksToResultObject,
} from './streamServerRenderedReactComponent';
import loadReactClientManifest from './loadReactClientManifest';
} from './streamServerRenderedReactComponent.ts';
import loadReactClientManifest from './loadReactClientManifest.cts';

const stringToStream = (str: string) => {
const stream = new PassThrough();
Expand Down Expand Up @@ -65,5 +65,5 @@ ReactOnRails.serverRenderRSCReactComponent = (options: RSCRenderParams) => {
}
};

export * from './types';
export * from './_types.ts';
export default ReactOnRails;
4 changes: 2 additions & 2 deletions node_package/src/StoreRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import CallbackRegistry from './CallbackRegistry';
import type { Store, StoreGenerator } from './types';
import CallbackRegistry from './CallbackRegistry.ts';
import type { Store, StoreGenerator } from './_types.ts';

const storeGeneratorRegistry = new CallbackRegistry<StoreGenerator>('store generator');
const hydratedStoreRegistry = new CallbackRegistry<Store>('hydrated store');
Expand Down
File renamed without changes.
4 changes: 2 additions & 2 deletions node_package/src/buildConsoleReplay.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import RenderUtils from './RenderUtils';
import scriptSanitizedVal from './scriptSanitizedVal';
import RenderUtils from './RenderUtils.ts';
import scriptSanitizedVal from './scriptSanitizedVal.ts';

declare global {
interface Console {
Expand Down
8 changes: 4 additions & 4 deletions node_package/src/clientStartup.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { type Context, isWindow } from './context';
import { type Context, isWindow } from './context.ts';
import {
renderOrHydrateForceLoadedComponents,
renderOrHydrateAllComponents,
hydrateForceLoadedStores,
hydrateAllStores,
unmountAll,
} from './ClientSideRenderer';
import { onPageLoaded, onPageUnloaded } from './pageLifecycle';
import { debugTurbolinks } from './turbolinksUtils';
} from './ClientSideRenderer.ts';
import { onPageLoaded, onPageUnloaded } from './pageLifecycle.ts';
import { debugTurbolinks } from './turbolinksUtils.ts';

export async function reactOnRailsPageLoaded() {
debugTurbolinks('reactOnRailsPageLoaded');
Expand Down
2 changes: 1 addition & 1 deletion node_package/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ReactOnRailsInternal as ReactOnRailsType, RailsContext } from './types';
import type { ReactOnRailsInternal as ReactOnRailsType, RailsContext } from './_types.ts';

declare global {
interface Window {
Expand Down
4 changes: 2 additions & 2 deletions node_package/src/createReactOutput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import type {
ReactComponent,
RenderFunction,
CreateReactOutputResult,
} from './types/index';
import { isServerRenderHash, isPromise } from './isServerRenderResult';
} from './_types.ts';
import { isServerRenderHash, isPromise } from './isServerRenderResult.ts';

/**
* Logic to either call the renderFunction or call React.createElement to get the
Expand Down
2 changes: 1 addition & 1 deletion node_package/src/handleError.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import * as ReactDOMServer from 'react-dom/server';
import type { ErrorOptions } from './types/index';
import type { ErrorOptions } from './_types.ts';

function handleRenderFunctionIssue(options: ErrorOptions): string {
const { e, name } = options;
Expand Down
2 changes: 1 addition & 1 deletion node_package/src/isRenderFunction.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// See discussion:
// https://discuss.reactjs.org/t/how-to-determine-if-js-object-is-react-component/2825/2
import { ReactComponentOrRenderFunction, RenderFunction } from './types/index';
import { ReactComponentOrRenderFunction, RenderFunction } from './_types.ts';

/**
* Used to determine we'll call be calling React.createElement on the component of if this is a
Expand Down
2 changes: 1 addition & 1 deletion node_package/src/isServerRenderResult.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CreateReactOutputResult, ServerRenderResult } from './types/index';
import type { CreateReactOutputResult, ServerRenderResult } from './_types.ts';

export function isServerRenderHash(testValue: CreateReactOutputResult): testValue is ServerRenderResult {
return !!(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default async function loadReactClientManifest(reactClientManifestFileNam
// React client manifest is uploaded to node renderer as an asset.
// Renderer copies assets to the same place as the server-bundle.js and rsc-bundle.js.
// Thus, the __dirname of this code is where we can find the manifest file.
// And we need to be .cts to use __dirname
const manifestPath = path.resolve(__dirname, reactClientManifestFileName);
const loadedReactClientManifest = loadedReactClientManifests.get(manifestPath);
if (loadedReactClientManifest) {
Expand Down
2 changes: 1 addition & 1 deletion node_package/src/pageLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
turbolinksSupported,
turboInstalled,
turbolinksVersion5,
} from './turbolinksUtils';
} from './turbolinksUtils.ts';

type PageLifecycleCallback = () => void | Promise<void>;
type PageState = 'load' | 'unload' | 'initial';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
/* eslint-disable react/no-deprecated,@typescript-eslint/no-deprecated */
import * as ReactDOM from 'react-dom';

const reactMajorVersion = Number(ReactDOM.version?.split('.')[0]) || 16;

// TODO: once we require React 18, we can remove this and inline everything guarded by it.
// Not the default export because others may be added for future React versions.
// eslint-disable-next-line import/prefer-default-export
export const supportsRootApi = reactMajorVersion >= 18;

export const canHydrate = supportsRootApi || !!ReactDOM.hydrate;

export const { unmountComponentAtNode } = ReactDOM;
Loading
Loading