react-on-rails
Advanced tools
Comparing version 14.0.5 to 15.0.0-alpha.0
@@ -6,3 +6,3 @@ "use strict"; | ||
var token = document.querySelector('meta[name="csrf-token"]'); | ||
if (token && (token instanceof window.HTMLMetaElement)) { | ||
if (token instanceof HTMLMetaElement) { | ||
return token.content; | ||
@@ -9,0 +9,0 @@ } |
@@ -9,3 +9,3 @@ declare global { | ||
} | ||
export declare function consoleReplay(): string; | ||
export default function buildConsoleReplay(): string; | ||
export declare function consoleReplay(customConsoleHistory?: typeof console['history'] | undefined, numberOfMessagesToSkip?: number): string; | ||
export default function buildConsoleReplay(customConsoleHistory?: typeof console['history'] | undefined, numberOfMessagesToSkip?: number): string; |
@@ -6,16 +6,27 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.consoleReplay = void 0; | ||
exports.consoleReplay = consoleReplay; | ||
exports.default = buildConsoleReplay; | ||
var RenderUtils_1 = __importDefault(require("./RenderUtils")); | ||
var scriptSanitizedVal_1 = __importDefault(require("./scriptSanitizedVal")); | ||
function consoleReplay() { | ||
function consoleReplay(customConsoleHistory, numberOfMessagesToSkip) { | ||
if (customConsoleHistory === void 0) { customConsoleHistory = undefined; } | ||
if (numberOfMessagesToSkip === void 0) { numberOfMessagesToSkip = 0; } | ||
// console.history is a global polyfill used in server rendering. | ||
// $FlowFixMe | ||
if (!(console.history instanceof Array)) { | ||
var consoleHistory = customConsoleHistory !== null && customConsoleHistory !== void 0 ? customConsoleHistory : console.history; | ||
if (!(Array.isArray(consoleHistory))) { | ||
return ''; | ||
} | ||
var lines = console.history.map(function (msg) { | ||
var lines = consoleHistory.slice(numberOfMessagesToSkip).map(function (msg) { | ||
var stringifiedList = msg.arguments.map(function (arg) { | ||
var val; | ||
try { | ||
val = (typeof arg === 'string' || arg instanceof String) ? arg : JSON.stringify(arg); | ||
if (typeof arg === 'string') { | ||
val = arg; | ||
} | ||
else if (arg instanceof String) { | ||
val = String(arg); | ||
} | ||
else { | ||
val = JSON.stringify(arg); | ||
} | ||
if (val === undefined) { | ||
@@ -34,6 +45,6 @@ val = 'undefined'; | ||
} | ||
exports.consoleReplay = consoleReplay; | ||
function buildConsoleReplay() { | ||
return RenderUtils_1.default.wrapInScriptTags('consoleReplayLog', consoleReplay()); | ||
function buildConsoleReplay(customConsoleHistory, numberOfMessagesToSkip) { | ||
if (customConsoleHistory === void 0) { customConsoleHistory = undefined; } | ||
if (numberOfMessagesToSkip === void 0) { numberOfMessagesToSkip = 0; } | ||
return RenderUtils_1.default.wrapInScriptTags('consoleReplayLog', consoleReplay(customConsoleHistory, numberOfMessagesToSkip)); | ||
} | ||
exports.default = buildConsoleReplay; |
@@ -15,3 +15,5 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.clientStartup = exports.reactOnRailsComponentLoaded = exports.reactOnRailsPageLoaded = void 0; | ||
exports.reactOnRailsPageLoaded = reactOnRailsPageLoaded; | ||
exports.reactOnRailsComponentLoaded = reactOnRailsComponentLoaded; | ||
exports.clientStartup = clientStartup; | ||
var react_dom_1 = __importDefault(require("react-dom")); | ||
@@ -167,3 +169,2 @@ var createReactOutput_1 = __importDefault(require("./createReactOutput")); | ||
} | ||
exports.reactOnRailsPageLoaded = reactOnRailsPageLoaded; | ||
function reactOnRailsComponentLoaded(domId) { | ||
@@ -184,3 +185,2 @@ debugTurbolinks("reactOnRailsComponentLoaded ".concat(domId)); | ||
} | ||
exports.reactOnRailsComponentLoaded = reactOnRailsComponentLoaded; | ||
function unmount(el) { | ||
@@ -276,2 +276,1 @@ var domNodeId = domNodeIdForEl(el); | ||
} | ||
exports.clientStartup = clientStartup; |
@@ -36,4 +36,5 @@ "use strict"; | ||
get: function (name) { | ||
if (registeredComponents.has(name)) { | ||
return registeredComponents.get(name); | ||
var registeredComponent = registeredComponents.get(name); | ||
if (registeredComponent !== undefined) { | ||
return registeredComponent; | ||
} | ||
@@ -40,0 +41,0 @@ var keys = Array.from(registeredComponents.keys()).join(', '); |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.default = context; | ||
/** | ||
@@ -12,2 +13,1 @@ * Get the context, be it window or global | ||
} | ||
exports.default = context; |
@@ -7,2 +7,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.default = createReactOutput; | ||
var react_1 = __importDefault(require("react")); | ||
@@ -63,2 +64,1 @@ var isServerRenderResult_1 = require("./isServerRenderResult"); | ||
} | ||
exports.default = createReactOutput; |
@@ -1,2 +0,2 @@ | ||
import { ReactComponentOrRenderFunction } from "./types/index"; | ||
import { ReactComponentOrRenderFunction, RenderFunction } from "./types/index"; | ||
/** | ||
@@ -8,2 +8,2 @@ * Used to determine we'll call be calling React.createElement on the component of if this is a | ||
*/ | ||
export default function isRenderFunction(component: ReactComponentOrRenderFunction): boolean; | ||
export default function isRenderFunction(component: ReactComponentOrRenderFunction): component is RenderFunction; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.default = isRenderFunction; | ||
/** | ||
@@ -10,5 +11,5 @@ * Used to determine we'll call be calling React.createElement on the component of if this is a | ||
function isRenderFunction(component) { | ||
var _a; | ||
// No for es5 or es6 React Component | ||
if (component.prototype && | ||
component.prototype.isReactComponent) { | ||
if ((_a = component.prototype) === null || _a === void 0 ? void 0 : _a.isReactComponent) { | ||
return false; | ||
@@ -26,2 +27,1 @@ } | ||
} | ||
exports.default = isRenderFunction; |
import type { CreateReactOutputResult, ServerRenderResult } from './types/index'; | ||
export declare function isServerRenderHash(testValue: CreateReactOutputResult): testValue is ServerRenderResult; | ||
export declare function isPromise(testValue: CreateReactOutputResult): testValue is Promise<string>; | ||
export declare function isPromise<T>(testValue: CreateReactOutputResult | Promise<T> | string | null): testValue is Promise<T>; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.isPromise = exports.isServerRenderHash = void 0; | ||
exports.isServerRenderHash = isServerRenderHash; | ||
exports.isPromise = isPromise; | ||
function isServerRenderHash(testValue) { | ||
@@ -10,6 +11,4 @@ return !!(testValue.renderedHtml || | ||
} | ||
exports.isServerRenderHash = isServerRenderHash; | ||
function isPromise(testValue) { | ||
return !!(testValue.then); | ||
return !!(testValue === null || testValue === void 0 ? void 0 : testValue.then); | ||
} | ||
exports.isPromise = isPromise; |
@@ -6,3 +6,5 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.reactRender = exports.reactHydrate = void 0; | ||
exports.reactHydrate = void 0; | ||
exports.reactRender = reactRender; | ||
exports.default = reactHydrateOrRender; | ||
var react_dom_1 = __importDefault(require("react-dom")); | ||
@@ -40,6 +42,4 @@ var reactApis_1 = require("./reactApis"); | ||
} | ||
exports.reactRender = reactRender; | ||
function reactHydrateOrRender(domNode, reactElement, hydrate) { | ||
return hydrate ? (0, exports.reactHydrate)(domNode, reactElement) : reactRender(domNode, reactElement); | ||
} | ||
exports.default = reactHydrateOrRender; |
@@ -36,3 +36,3 @@ "use strict"; | ||
var StoreRegistry_1 = __importDefault(require("./StoreRegistry")); | ||
var serverRenderReactComponent_1 = __importDefault(require("./serverRenderReactComponent")); | ||
var serverRenderReactComponent_1 = __importStar(require("./serverRenderReactComponent")); | ||
var buildConsoleReplay_1 = __importDefault(require("./buildConsoleReplay")); | ||
@@ -64,14 +64,17 @@ var createReactOutput_1 = __importDefault(require("./createReactOutput")); | ||
}, | ||
registerStore: function (stores) { | ||
this.registerStoreGenerators(stores); | ||
}, | ||
/** | ||
* Allows registration of store generators to be used by multiple react components on one Rails | ||
* Allows registration of store generators to be used by multiple React components on one Rails | ||
* view. store generators are functions that take one arg, props, and return a store. Note that | ||
* the setStore API is different in that it's the actual store hydrated with props. | ||
* @param stores (keys are store names, values are the store generators) | ||
* @param storeGenerators (keys are store names, values are the store generators) | ||
*/ | ||
registerStore: function (stores) { | ||
if (!stores) { | ||
throw new Error('Called ReactOnRails.registerStores with a null or undefined, rather than ' + | ||
registerStoreGenerators: function (storeGenerators) { | ||
if (!storeGenerators) { | ||
throw new Error('Called ReactOnRails.registerStoreGenerators with a null or undefined, rather than ' + | ||
'an Object with keys being the store names and the values are the store generators.'); | ||
} | ||
StoreRegistry_1.default.register(stores); | ||
StoreRegistry_1.default.register(storeGenerators); | ||
}, | ||
@@ -143,3 +146,3 @@ /** | ||
* Returns header with csrf authenticity token and XMLHttpRequest | ||
* @param {*} other headers | ||
* @param otherHeaders Other headers | ||
* @returns {*} header | ||
@@ -164,3 +167,3 @@ */ | ||
* Allows retrieval of the store generator by name. This is used internally by ReactOnRails after | ||
* a rails form loads to prepare stores. | ||
* a Rails form loads to prepare stores. | ||
* @param name | ||
@@ -233,2 +236,9 @@ * @returns Redux Store generator function | ||
/** | ||
* Used by server rendering by Rails | ||
* @param options | ||
*/ | ||
streamServerRenderedReactComponent: function (options) { | ||
return (0, serverRenderReactComponent_1.streamServerRenderedReactComponent)(options); | ||
}, | ||
/** | ||
* Used by Rails to catch errors in rendering | ||
@@ -235,0 +245,0 @@ * @param options |
@@ -1,4 +0,6 @@ | ||
import type { RenderParams, RenderResult } from './types/index'; | ||
import { Readable } from 'stream'; | ||
import type { RenderParams, RenderResult } from './types'; | ||
declare function serverRenderReactComponentInternal(options: RenderParams): null | string | Promise<RenderResult>; | ||
declare const serverRenderReactComponent: typeof serverRenderReactComponentInternal; | ||
export declare const streamServerRenderedReactComponent: (options: RenderParams) => Readable; | ||
export default serverRenderReactComponent; |
"use strict"; | ||
var __assign = (this && this.__assign) || function () { | ||
__assign = Object.assign || function(t) { | ||
for (var s, i = 1, n = arguments.length; i < n; i++) { | ||
s = arguments[i]; | ||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) | ||
t[p] = s[p]; | ||
} | ||
return t; | ||
}; | ||
return __assign.apply(this, arguments); | ||
}; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
@@ -12,4 +23,4 @@ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
var __generator = (this && this.__generator) || function (thisArg, body) { | ||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; | ||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; | ||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); | ||
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; | ||
function verb(n) { return function (v) { return step([n, v]); }; } | ||
@@ -43,3 +54,5 @@ function step(op) { | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.streamServerRenderedReactComponent = void 0; | ||
var server_1 = __importDefault(require("react-dom/server")); | ||
var stream_1 = require("stream"); | ||
var ComponentRegistry_1 = __importDefault(require("./ComponentRegistry")); | ||
@@ -50,135 +63,147 @@ var createReactOutput_1 = __importDefault(require("./createReactOutput")); | ||
var handleError_1 = __importDefault(require("./handleError")); | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
function serverRenderReactComponentInternal(options) { | ||
var _this = this; | ||
var name = options.name, domNodeId = options.domNodeId, trace = options.trace, props = options.props, railsContext = options.railsContext, renderingReturnsPromises = options.renderingReturnsPromises, throwJsErrors = options.throwJsErrors; | ||
var renderResult = null; | ||
var hasErrors = false; | ||
var renderingError = null; | ||
function convertToError(e) { | ||
return e instanceof Error ? e : new Error(String(e)); | ||
} | ||
function validateComponent(componentObj, componentName) { | ||
if (componentObj.isRenderer) { | ||
throw new Error("Detected a renderer while server rendering component '".concat(componentName, "'. See https://github.com/shakacode/react_on_rails#renderer-functions")); | ||
} | ||
} | ||
function processServerRenderHash(result, options) { | ||
var redirectLocation = result.redirectLocation, routeError = result.routeError; | ||
var hasErrors = !!routeError; | ||
if (hasErrors) { | ||
console.error("React Router ERROR: ".concat(JSON.stringify(routeError))); | ||
} | ||
var htmlResult; | ||
if (redirectLocation) { | ||
if (options.trace) { | ||
var redirectPath = redirectLocation.pathname + redirectLocation.search; | ||
console.log("ROUTER REDIRECT: ".concat(options.componentName, " to dom node with id: ").concat(options.domNodeId, ", redirect to ").concat(redirectPath)); | ||
} | ||
// For redirects on server rendering, we can't stop Rails from returning the same result. | ||
// Possibly, someday, we could have the Rails server redirect. | ||
htmlResult = ''; | ||
} | ||
else { | ||
htmlResult = result.renderedHtml; | ||
} | ||
return { result: htmlResult, hasErrors: hasErrors }; | ||
} | ||
function processPromise(result, renderingReturnsPromises) { | ||
if (!renderingReturnsPromises) { | ||
console.error('Your render function returned a Promise, which is only supported by a node renderer, not ExecJS.'); | ||
// If the app is using server rendering with ExecJS, then the promise will not be awaited. | ||
// And when a promise is passed to JSON.stringify, it will be converted to '{}'. | ||
return '{}'; | ||
} | ||
return result; | ||
} | ||
function processReactElement(result) { | ||
try { | ||
var componentObj = ComponentRegistry_1.default.get(name); | ||
if (componentObj.isRenderer) { | ||
throw new Error("Detected a renderer while server rendering component '".concat(name, "'. See https://github.com/shakacode/react_on_rails#renderer-functions")); | ||
} | ||
var reactRenderingResult_1 = (0, createReactOutput_1.default)({ | ||
componentObj: componentObj, | ||
domNodeId: domNodeId, | ||
trace: trace, | ||
props: props, | ||
railsContext: railsContext, | ||
}); | ||
var processServerRenderHash = function () { | ||
// We let the client side handle any redirect | ||
// Set hasErrors in case we want to throw a Rails exception | ||
hasErrors = !!reactRenderingResult_1.routeError; | ||
if (hasErrors) { | ||
console.error("React Router ERROR: ".concat(JSON.stringify(reactRenderingResult_1.routeError))); | ||
return server_1.default.renderToString(result); | ||
} | ||
catch (error) { | ||
console.error("Invalid call to renderToString. Possibly you have a renderFunction, a function that already\ncalls renderToString, that takes one parameter. You need to add an extra unused parameter to identify this function\nas a renderFunction and not a simple React Function Component."); | ||
throw error; | ||
} | ||
} | ||
function processRenderingResult(result, options) { | ||
if ((0, isServerRenderResult_1.isServerRenderHash)(result)) { | ||
return processServerRenderHash(result, options); | ||
} | ||
if ((0, isServerRenderResult_1.isPromise)(result)) { | ||
return { result: processPromise(result, options.renderingReturnsPromises), hasErrors: false }; | ||
} | ||
return { result: processReactElement(result), hasErrors: false }; | ||
} | ||
function handleRenderingError(e, options) { | ||
if (options.throwJsErrors) { | ||
throw e; | ||
} | ||
var error = convertToError(e); | ||
return { | ||
hasErrors: true, | ||
result: (0, handleError_1.default)({ e: error, name: options.componentName, serverSide: true }), | ||
error: error, | ||
}; | ||
} | ||
function createResultObject(html, consoleReplayScript, renderState) { | ||
return { | ||
html: html, | ||
consoleReplayScript: consoleReplayScript, | ||
hasErrors: renderState.hasErrors, | ||
renderingError: renderState.error && { message: renderState.error.message, stack: renderState.error.stack }, | ||
isShellReady: 'isShellReady' in renderState ? renderState.isShellReady : undefined, | ||
}; | ||
} | ||
function createPromiseResult(renderState, componentName, throwJsErrors) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var consoleHistory, html, consoleReplayScript, e_1, errorRenderState, consoleReplayScript; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
consoleHistory = console.history; | ||
_a.label = 1; | ||
case 1: | ||
_a.trys.push([1, 3, , 4]); | ||
return [4 /*yield*/, renderState.result]; | ||
case 2: | ||
html = _a.sent(); | ||
consoleReplayScript = (0, buildConsoleReplay_1.default)(consoleHistory); | ||
return [2 /*return*/, createResultObject(html, consoleReplayScript, renderState)]; | ||
case 3: | ||
e_1 = _a.sent(); | ||
errorRenderState = handleRenderingError(e_1, { componentName: componentName, throwJsErrors: throwJsErrors }); | ||
consoleReplayScript = (0, buildConsoleReplay_1.default)(consoleHistory); | ||
return [2 /*return*/, createResultObject(errorRenderState.result, consoleReplayScript, renderState)]; | ||
case 4: return [2 /*return*/]; | ||
} | ||
if (reactRenderingResult_1.redirectLocation) { | ||
if (trace) { | ||
var redirectLocation = reactRenderingResult_1.redirectLocation; | ||
var redirectPath = redirectLocation.pathname + redirectLocation.search; | ||
console.log(" ROUTER REDIRECT: ".concat(name, " to dom node with id: ").concat(domNodeId, ", redirect to ").concat(redirectPath)); | ||
} | ||
// For redirects on server rendering, we can't stop Rails from returning the same result. | ||
// Possibly, someday, we could have the rails server redirect. | ||
return ''; | ||
} | ||
return reactRenderingResult_1.renderedHtml; | ||
}; | ||
var processPromise = function () { | ||
if (!renderingReturnsPromises) { | ||
console.error('Your render function returned a Promise, which is only supported by a node renderer, not ExecJS.'); | ||
} | ||
return reactRenderingResult_1; | ||
}; | ||
var processReactElement = function () { | ||
try { | ||
return server_1.default.renderToString(reactRenderingResult_1); | ||
} | ||
catch (error) { | ||
console.error("Invalid call to renderToString. Possibly you have a renderFunction, a function that already\ncalls renderToString, that takes one parameter. You need to add an extra unused parameter to identify this function\nas a renderFunction and not a simple React Function Component."); | ||
throw error; | ||
} | ||
}; | ||
if ((0, isServerRenderResult_1.isServerRenderHash)(reactRenderingResult_1)) { | ||
renderResult = processServerRenderHash(); | ||
} | ||
else if ((0, isServerRenderResult_1.isPromise)(reactRenderingResult_1)) { | ||
renderResult = processPromise(); | ||
} | ||
else { | ||
renderResult = processReactElement(); | ||
} | ||
} | ||
catch (e) { | ||
if (throwJsErrors) { | ||
throw e; | ||
} | ||
hasErrors = true; | ||
renderResult = (0, handleError_1.default)({ | ||
e: e, | ||
name: name, | ||
serverSide: true, | ||
}); | ||
renderingError = e; | ||
}); | ||
} | ||
function createFinalResult(renderState, componentName, throwJsErrors) { | ||
var result = renderState.result; | ||
if ((0, isServerRenderResult_1.isPromise)(result)) { | ||
return createPromiseResult(__assign(__assign({}, renderState), { result: result }), componentName, throwJsErrors); | ||
} | ||
var consoleReplayScript = (0, buildConsoleReplay_1.default)(); | ||
var addRenderingErrors = function (resultObject, renderError) { | ||
resultObject.renderingError = { | ||
message: renderError.message, | ||
stack: renderError.stack, | ||
}; | ||
return JSON.stringify(createResultObject(result, consoleReplayScript, renderState)); | ||
} | ||
function serverRenderReactComponentInternal(options) { | ||
var componentName = options.name, domNodeId = options.domNodeId, trace = options.trace, props = options.props, railsContext = options.railsContext, renderingReturnsPromises = options.renderingReturnsPromises, throwJsErrors = options.throwJsErrors; | ||
var renderState = { | ||
result: null, | ||
hasErrors: false, | ||
}; | ||
if (renderingReturnsPromises) { | ||
var resolveRenderResult = function () { return __awaiter(_this, void 0, void 0, function () { | ||
var promiseResult, e_1; | ||
var _a; | ||
return __generator(this, function (_b) { | ||
switch (_b.label) { | ||
case 0: | ||
_b.trys.push([0, 2, , 3]); | ||
_a = {}; | ||
return [4 /*yield*/, renderResult]; | ||
case 1: | ||
promiseResult = (_a.html = _b.sent(), | ||
_a.consoleReplayScript = consoleReplayScript, | ||
_a.hasErrors = hasErrors, | ||
_a); | ||
return [3 /*break*/, 3]; | ||
case 2: | ||
e_1 = _b.sent(); | ||
if (throwJsErrors) { | ||
throw e_1; | ||
} | ||
promiseResult = { | ||
html: (0, handleError_1.default)({ | ||
e: e_1, | ||
name: name, | ||
serverSide: true, | ||
}), | ||
consoleReplayScript: consoleReplayScript, | ||
hasErrors: true, | ||
}; | ||
renderingError = e_1; | ||
return [3 /*break*/, 3]; | ||
case 3: | ||
if (renderingError !== null) { | ||
addRenderingErrors(promiseResult, renderingError); | ||
} | ||
return [2 /*return*/, promiseResult]; | ||
} | ||
}); | ||
}); }; | ||
return resolveRenderResult(); | ||
try { | ||
var componentObj = ComponentRegistry_1.default.get(componentName); | ||
validateComponent(componentObj, componentName); | ||
// Renders the component or executes the render function | ||
// - If the registered component is a React element or component, it renders it | ||
// - If it's a render function, it executes the function and processes the result: | ||
// - For React elements or components, it renders them | ||
// - For promises, it returns them without awaiting (for async rendering) | ||
// - For other values (e.g., strings), it returns them directly | ||
// Note: Only synchronous operations are performed at this stage | ||
var reactRenderingResult = (0, createReactOutput_1.default)({ componentObj: componentObj, domNodeId: domNodeId, trace: trace, props: props, railsContext: railsContext }); | ||
// Processes the result from createReactOutput: | ||
// 1. Converts React elements to HTML strings | ||
// 2. Returns rendered HTML from serverRenderHash | ||
// 3. Handles promises for async rendering | ||
renderState = processRenderingResult(reactRenderingResult, { componentName: componentName, domNodeId: domNodeId, trace: trace, renderingReturnsPromises: renderingReturnsPromises }); | ||
} | ||
var result = { | ||
html: renderResult, | ||
consoleReplayScript: consoleReplayScript, | ||
hasErrors: hasErrors, | ||
}; | ||
if (renderingError) { | ||
addRenderingErrors(result, renderingError); | ||
catch (e) { | ||
renderState = handleRenderingError(e, { componentName: componentName, throwJsErrors: throwJsErrors }); | ||
} | ||
return JSON.stringify(result); | ||
// Finalize the rendering result and prepare it for server response | ||
// 1. Builds the consoleReplayScript for client-side console replay | ||
// 2. Extract the result from promise (if needed) by awaiting it | ||
// 3. Constructs a JSON object with the following properties: | ||
// - html: string | null (The rendered component HTML) | ||
// - consoleReplayScript: string (Script to replay console outputs on the client) | ||
// - hasErrors: boolean (Indicates if any errors occurred during rendering) | ||
// - renderingError: Error | null (The error object if an error occurred, null otherwise) | ||
// 4. For Promise results, it awaits resolution before creating the final JSON | ||
return createFinalResult(renderState, componentName, throwJsErrors); | ||
} | ||
@@ -192,5 +217,111 @@ var serverRenderReactComponent = function (options) { | ||
// See `RubyEmbeddedJavaScript.console_polyfill` for initialization. | ||
// This is necessary when ExecJS and old versions of node renderer are used. | ||
// New versions of node renderer reset the console history automatically. | ||
console.history = []; | ||
} | ||
}; | ||
var stringToStream = function (str) { | ||
var stream = new stream_1.PassThrough(); | ||
stream.write(str); | ||
stream.end(); | ||
return stream; | ||
}; | ||
var transformRenderStreamChunksToResultObject = function (renderState) { | ||
var consoleHistory = console.history; | ||
var previouslyReplayedConsoleMessages = 0; | ||
var transformStream = new stream_1.PassThrough({ | ||
transform: function (chunk, _, callback) { | ||
var htmlChunk = chunk.toString(); | ||
var consoleReplayScript = (0, buildConsoleReplay_1.default)(consoleHistory, previouslyReplayedConsoleMessages); | ||
previouslyReplayedConsoleMessages = (consoleHistory === null || consoleHistory === void 0 ? void 0 : consoleHistory.length) || 0; | ||
var jsonChunk = JSON.stringify(createResultObject(htmlChunk, consoleReplayScript, renderState)); | ||
this.push("".concat(jsonChunk, "\n")); | ||
callback(); | ||
} | ||
}); | ||
var pipedStream = null; | ||
var pipeToTransform = function (pipeableStream) { | ||
pipeableStream.pipe(transformStream); | ||
pipedStream = pipeableStream; | ||
}; | ||
// We need to wrap the transformStream in a Readable stream to properly handle errors: | ||
// 1. If we returned transformStream directly, we couldn't emit errors into it externally | ||
// 2. If an error is emitted into the transformStream, it would cause the render to fail | ||
// 3. By wrapping in Readable.from(), we can explicitly emit errors into the readableStream without affecting the transformStream | ||
// Note: Readable.from can merge multiple chunks into a single chunk, so we need to ensure that we can separate them later | ||
var readableStream = stream_1.Readable.from(transformStream); | ||
var writeChunk = function (chunk) { return transformStream.write(chunk); }; | ||
var emitError = function (error) { return readableStream.emit('error', error); }; | ||
var endStream = function () { | ||
transformStream.end(); | ||
pipedStream === null || pipedStream === void 0 ? void 0 : pipedStream.abort(); | ||
}; | ||
return { readableStream: readableStream, pipeToTransform: pipeToTransform, writeChunk: writeChunk, emitError: emitError, endStream: endStream }; | ||
}; | ||
var streamRenderReactComponent = function (reactRenderingResult, options) { | ||
var componentName = options.name, throwJsErrors = options.throwJsErrors; | ||
var renderState = { | ||
result: null, | ||
hasErrors: false, | ||
isShellReady: false | ||
}; | ||
var _a = transformRenderStreamChunksToResultObject(renderState), readableStream = _a.readableStream, pipeToTransform = _a.pipeToTransform, writeChunk = _a.writeChunk, emitError = _a.emitError, endStream = _a.endStream; | ||
var renderingStream = server_1.default.renderToPipeableStream(reactRenderingResult, { | ||
onShellError: function (e) { | ||
var error = convertToError(e); | ||
renderState.hasErrors = true; | ||
renderState.error = error; | ||
if (throwJsErrors) { | ||
emitError(error); | ||
} | ||
var errorHtml = (0, handleError_1.default)({ e: error, name: componentName, serverSide: true }); | ||
writeChunk(errorHtml); | ||
endStream(); | ||
}, | ||
onShellReady: function () { | ||
renderState.isShellReady = true; | ||
pipeToTransform(renderingStream); | ||
}, | ||
onError: function (e) { | ||
if (!renderState.isShellReady) { | ||
return; | ||
} | ||
var error = convertToError(e); | ||
if (throwJsErrors) { | ||
emitError(error); | ||
} | ||
renderState.hasErrors = true; | ||
renderState.error = error; | ||
}, | ||
}); | ||
return readableStream; | ||
}; | ||
var streamServerRenderedReactComponent = function (options) { | ||
var componentName = options.name, domNodeId = options.domNodeId, trace = options.trace, props = options.props, railsContext = options.railsContext, throwJsErrors = options.throwJsErrors; | ||
try { | ||
var componentObj = ComponentRegistry_1.default.get(componentName); | ||
validateComponent(componentObj, componentName); | ||
var reactRenderingResult = (0, createReactOutput_1.default)({ | ||
componentObj: componentObj, | ||
domNodeId: domNodeId, | ||
trace: trace, | ||
props: props, | ||
railsContext: railsContext, | ||
}); | ||
if ((0, isServerRenderResult_1.isServerRenderHash)(reactRenderingResult) || (0, isServerRenderResult_1.isPromise)(reactRenderingResult)) { | ||
throw new Error('Server rendering of streams is not supported for server render hashes or promises.'); | ||
} | ||
return streamRenderReactComponent(reactRenderingResult, options); | ||
} | ||
catch (e) { | ||
if (throwJsErrors) { | ||
throw e; | ||
} | ||
var error = convertToError(e); | ||
var htmlResult = (0, handleError_1.default)({ e: error, name: componentName, serverSide: true }); | ||
var jsonResult = JSON.stringify(createResultObject(htmlResult, (0, buildConsoleReplay_1.default)(), { hasErrors: true, error: error, result: null })); | ||
return stringToStream(jsonResult); | ||
} | ||
}; | ||
exports.streamServerRenderedReactComponent = streamServerRenderedReactComponent; | ||
exports.default = serverRenderReactComponent; |
@@ -1,3 +0,2 @@ | ||
import type { StoreGenerator } from './types'; | ||
type Store = any; | ||
import type { Store, StoreGenerator } from './types'; | ||
declare const _default: { | ||
@@ -9,3 +8,3 @@ /** | ||
register(storeGenerators: { | ||
[id: string]: any; | ||
[id: string]: StoreGenerator; | ||
}): void; | ||
@@ -12,0 +11,0 @@ /** |
@@ -53,4 +53,5 @@ "use strict"; | ||
getStoreGenerator: function (name) { | ||
if (registeredStoreGenerators.has(name)) { | ||
return registeredStoreGenerators.get(name); | ||
var registeredStoreGenerator = registeredStoreGenerators.get(name); | ||
if (registeredStoreGenerator) { | ||
return registeredStoreGenerator; | ||
} | ||
@@ -57,0 +58,0 @@ var storeKeys = Array.from(registeredStoreGenerators.keys()).join(', '); |
import type { ReactElement, ReactNode, Component, ComponentType } from 'react'; | ||
type Store = any; | ||
import type { Readable } from 'stream'; | ||
type Store = unknown; | ||
type ReactComponent = ComponentType<any> | string; | ||
@@ -43,12 +44,40 @@ export interface RailsContext { | ||
type RenderFunctionResult = ReactComponent | ServerRenderResult | Promise<string>; | ||
/** | ||
* Render functions are used to create dynamic React components or server-rendered HTML with side effects. | ||
* They receive two arguments: props and railsContext. | ||
* | ||
* @param props - The component props passed to the render function | ||
* @param railsContext - The Rails context object containing environment information | ||
* @returns A string, React component, React element, or a Promise resolving to a string | ||
* | ||
* @remarks | ||
* To distinguish a render function from a React Function Component: | ||
* 1. Ensure it accepts two parameters (props and railsContext), even if railsContext is unused, or | ||
* 2. Set the `renderFunction` property to `true` on the function object. | ||
* | ||
* If neither condition is met, it will be treated as a React Function Component, | ||
* and ReactDOMServer will attempt to render it. | ||
* | ||
* @example | ||
* // Option 1: Two-parameter function | ||
* const renderFunction = (props, railsContext) => { ... }; | ||
* | ||
* // Option 2: Using renderFunction property | ||
* const anotherRenderFunction = (props) => { ... }; | ||
* anotherRenderFunction.renderFunction = true; | ||
*/ | ||
interface RenderFunction { | ||
(props?: any, railsContext?: RailsContext, domNodeId?: string): RenderFunctionResult; | ||
renderFunction?: boolean; | ||
renderFunction?: true; | ||
} | ||
type ReactComponentOrRenderFunction = ReactComponent | RenderFunction; | ||
export type { // eslint-disable-line import/prefer-default-export | ||
ReactComponentOrRenderFunction, ReactComponent, AuthenticityHeaders, RenderFunction, RenderFunctionResult, StoreGenerator, CreateReactOutputResult, ServerRenderResult, }; | ||
ReactComponentOrRenderFunction, ReactComponent, AuthenticityHeaders, RenderFunction, RenderFunctionResult, Store, StoreGenerator, CreateReactOutputResult, ServerRenderResult, }; | ||
export interface RegisteredComponent { | ||
name: string; | ||
component: ReactComponentOrRenderFunction; | ||
/** | ||
* Indicates if the registered component is a RenderFunction | ||
* @see RenderFunction for more details on its behavior and usage. | ||
*/ | ||
renderFunction: boolean; | ||
@@ -72,8 +101,7 @@ isRenderer: boolean; | ||
} | ||
interface FileError extends Error { | ||
fileName: string; | ||
lineNumber: string; | ||
} | ||
export interface ErrorOptions { | ||
e: FileError; | ||
e: Error & { | ||
fileName?: string; | ||
lineNumber?: string; | ||
}; | ||
name?: string; | ||
@@ -83,6 +111,3 @@ jsCode?: string; | ||
} | ||
export interface RenderingError { | ||
message: string; | ||
stack: string; | ||
} | ||
export type RenderingError = Pick<Error, 'message' | 'stack'>; | ||
export interface RenderResult { | ||
@@ -93,2 +118,3 @@ html: string | null; | ||
renderingError?: RenderingError; | ||
isShellReady?: boolean; | ||
} | ||
@@ -104,5 +130,9 @@ export interface Root { | ||
}): void; | ||
/** @deprecated Use registerStoreGenerators instead */ | ||
registerStore(stores: { | ||
[id: string]: Store; | ||
[id: string]: StoreGenerator; | ||
}): void; | ||
registerStoreGenerators(storeGenerators: { | ||
[id: string]: StoreGenerator; | ||
}): void; | ||
getStore(name: string, throwIfMissing?: boolean): Store | undefined; | ||
@@ -126,2 +156,3 @@ setOptions(newOptions: { | ||
serverRenderReactComponent(options: RenderParams): null | string | Promise<RenderResult>; | ||
streamServerRenderedReactComponent(options: RenderParams): Readable; | ||
handleError(options: ErrorOptions): string | undefined; | ||
@@ -128,0 +159,0 @@ buildConsoleReplay(): string; |
{ | ||
"name": "react-on-rails", | ||
"version": "14.0.5", | ||
"version": "15.0.0-alpha.0", | ||
"description": "react-on-rails JavaScript for react_on_rails Ruby gem", | ||
@@ -18,4 +18,4 @@ "main": "node_package/lib/ReactOnRails.js", | ||
"@types/jest": "^29.0.0", | ||
"@types/react": "^17.0.0", | ||
"@types/react-dom": "^17.0.0", | ||
"@types/react": "^18.2.0", | ||
"@types/react-dom": "^18.2.0", | ||
"@types/turbolinks": "^5.2.2", | ||
@@ -43,8 +43,8 @@ "@types/webpack-env": "^1.18.4", | ||
"prop-types": "^15.8.1", | ||
"react": "^17.0.0", | ||
"react-dom": "^17.0.0", | ||
"react": "18.3.0-canary-670811593-20240322", | ||
"react-dom": "18.3.0-canary-670811593-20240322", | ||
"react-transform-hmr": "^1.0.4", | ||
"redux": "^4.2.1", | ||
"ts-jest": "^29.1.0", | ||
"typescript": "^5.3.3" | ||
"typescript": "^5.6.2" | ||
}, | ||
@@ -51,0 +51,0 @@ "dependencies": { |
@@ -20,2 +20,3 @@  | ||
# News | ||
* [React on Rails Pro](https://www.shakacode.com/react-on-rails-pro/) supports the latest features of React 18, including [React Server Components](https://react.dev/reference/rsc/server-components) and [streaming](https://react.dev/reference/react-dom/server/renderToPipeableStream). Contact [Justin Gordon](mailto:justin@shakacode.com) for more information. | ||
* ShakaCode now maintains the official successor to `rails/webpacker`, [`shakapacker`](https://github.com/shakacode/shakapacker). | ||
@@ -22,0 +23,0 @@ * Project is updated to support Rails 7 and Shakapacker v6+! |
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
120980
38
1641
147
1