Skip to content
Merged
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
27 changes: 12 additions & 15 deletions node_package/src/Authenticity.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import type { AuthenticityHeaders } from './types/index';

export default {
authenticityToken(): string | null {
const token = document.querySelector('meta[name="csrf-token"]');
if (token instanceof HTMLMetaElement) {
return token.content;
}
return null;
},
export function authenticityToken(): string | null {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const token = document.querySelector('meta[name="csrf-token"]');
if (token instanceof HTMLMetaElement) {
return token.content;
}
return null;
}

authenticityHeaders(otherHeaders: Record<string, string> = {}): AuthenticityHeaders {
return Object.assign(otherHeaders, {
'X-CSRF-Token': this.authenticityToken(),
'X-Requested-With': 'XMLHttpRequest',
});
},
};
export const authenticityHeaders = (otherHeaders: Record<string, string> = {}): AuthenticityHeaders =>
Object.assign(otherHeaders, {
'X-CSRF-Token': authenticityToken(),
'X-Requested-With': 'XMLHttpRequest',
});
96 changes: 45 additions & 51 deletions node_package/src/ComponentRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,55 +4,49 @@ import CallbackRegistry from './CallbackRegistry';

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

export default {
/**
* @param components { component1: component1, component2: component2, etc. }
*/
register(components: Record<string, ReactComponentOrRenderFunction>): void {
Object.keys(components).forEach((name) => {
if (componentRegistry.has(name)) {
console.warn('Called register for component that is already registered', name);
}

const component = components[name];
if (!component) {
throw new Error(`Called register with null component named ${name}`);
}

const renderFunction = isRenderFunction(component);
const isRenderer = renderFunction && component.length === 3;

componentRegistry.set(name, {
name,
component,
renderFunction,
isRenderer,
});
/**
* @param components { component1: component1, component2: component2, etc. }
*/
export function register(components: Record<string, ReactComponentOrRenderFunction>): void {
Object.keys(components).forEach((name) => {
if (componentRegistry.has(name)) {
console.warn('Called register for component that is already registered', name);
}

const component = components[name];
if (!component) {
throw new Error(`Called register with null component named ${name}`);
}

const renderFunction = isRenderFunction(component);
const isRenderer = renderFunction && component.length === 3;

componentRegistry.set(name, {
name,
component,
renderFunction,
isRenderer,
});
},

/**
* @param name
* @returns { name, component, isRenderFunction, isRenderer }
*/
get(name: string): RegisteredComponent {
return componentRegistry.get(name);
},

getOrWaitForComponent(name: string): Promise<RegisteredComponent> {
return componentRegistry.getOrWaitForItem(name);
},

/**
* Get a Map containing all registered components. Useful for debugging.
* @returns Map where key is the component name and values are the
* { name, component, renderFunction, isRenderer}
*/
components(): Map<string, RegisteredComponent> {
return componentRegistry.getAll();
},

clear(): void {
componentRegistry.clear();
},
};
});
}

/**
* @param name
* @returns { name, component, isRenderFunction, isRenderer }
*/
export const get = (name: string): RegisteredComponent => componentRegistry.get(name);

export const getOrWaitForComponent = (name: string): Promise<RegisteredComponent> =>
componentRegistry.getOrWaitForItem(name);

/**
* Get a Map containing all registered components. Useful for debugging.
* @returns Map where key is the component name and values are the
* { name, component, renderFunction, isRenderer}
*/
export const components = (): Map<string, RegisteredComponent> => componentRegistry.getAll();

/** @internal Exported only for tests */
export function clear(): void {
componentRegistry.clear();
}
6 changes: 3 additions & 3 deletions node_package/src/ReactOnRails.client.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { ReactElement } from 'react';
import * as ClientStartup from './clientStartup';
import { renderOrHydrateComponent, hydrateStore } from './ClientSideRenderer';
import ComponentRegistry from './ComponentRegistry';
import StoreRegistry from './StoreRegistry';
import * as ComponentRegistry from './ComponentRegistry';
import * as StoreRegistry from './StoreRegistry';
import buildConsoleReplay from './buildConsoleReplay';
import createReactOutput from './createReactOutput';
import Authenticity from './Authenticity';
import * as Authenticity from './Authenticity';
import context from './context';
import type {
RegisteredComponent,
Expand Down
16 changes: 8 additions & 8 deletions node_package/src/RenderUtils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
export default {
wrapInScriptTags(scriptId: string, scriptBody: string): string {
if (!scriptBody) {
return '';
}
// eslint-disable-next-line import/prefer-default-export -- only one export for now, but others may be added later
export function wrapInScriptTags(scriptId: string, scriptBody: string): string {
if (!scriptBody) {
return '';
}

return `\n<script id="${scriptId}">
return `
<script id="${scriptId}">
${scriptBody}
</script>`;
},
};
}
176 changes: 83 additions & 93 deletions node_package/src/StoreRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,112 +4,102 @@ import type { Store, StoreGenerator } from './types';
const storeGeneratorRegistry = new CallbackRegistry<StoreGenerator>('store generator');
const hydratedStoreRegistry = new CallbackRegistry<Store>('hydrated store');

export default {
/**
* Register a store generator, a function that takes props and returns a store.
* @param storeGenerators { name1: storeGenerator1, name2: storeGenerator2 }
*/
register(storeGenerators: Record<string, StoreGenerator>): void {
Object.keys(storeGenerators).forEach((name) => {
if (storeGeneratorRegistry.has(name)) {
console.warn('Called registerStore for store that is already registered', name);
}
/**
* Register a store generator, a function that takes props and returns a store.
* @param storeGenerators { name1: storeGenerator1, name2: storeGenerator2 }
*/
export function register(storeGenerators: Record<string, StoreGenerator>): void {
Object.keys(storeGenerators).forEach((name) => {
if (storeGeneratorRegistry.has(name)) {
console.warn('Called registerStore for store that is already registered', name);
}

const store = storeGenerators[name];
if (!store) {
throw new Error(
'Called ReactOnRails.registerStores with a null or undefined as a value ' +
`for the store generator with key ${name}.`,
);
}
const storeGenerator = storeGenerators[name];
if (!storeGenerator) {
throw new Error(
'Called ReactOnRails.registerStoreGenerators with a null or undefined as a value ' +
`for the store generator with key ${name}.`,
);
}

storeGeneratorRegistry.set(name, store);
});
},
storeGeneratorRegistry.set(name, storeGenerator);
});
}

/**
* Used by components to get the hydrated store which contains props.
* @param name
* @param throwIfMissing Defaults to true. Set to false to have this call return undefined if
* there is no store with the given name.
* @returns Redux Store, possibly hydrated
*/
getStore(name: string, throwIfMissing = true): Store | undefined {
try {
return hydratedStoreRegistry.get(name);
} catch (error) {
if (hydratedStoreRegistry.getAll().size === 0) {
const msg = `There are no stores hydrated and you are requesting the store ${name}.
/**
* Used by components to get the hydrated store which contains props.
* @param name
* @param throwIfMissing Defaults to true. Set to false to have this call return undefined if
* there is no store with the given name.
* @returns Redux Store, possibly hydrated
*/
export function getStore(name: string, throwIfMissing = true): Store | undefined {
try {
return hydratedStoreRegistry.get(name);
} catch (error) {
if (hydratedStoreRegistry.getAll().size === 0) {
const msg = `There are no stores hydrated and you are requesting the store ${name}.
This can happen if you are server rendering and either:
1. You do not call redux_store near the top of your controller action's view (not the layout)
and before any call to react_component.
2. You do not render redux_store_hydration_data anywhere on your page.`;
throw new Error(msg);
}
throw new Error(msg);
}

if (throwIfMissing) {
throw error;
}
return undefined;
if (throwIfMissing) {
throw error;
}
},
return undefined;
}
}

/**
* Internally used function to get the store creator that was passed to `register`.
* @param name
* @returns storeCreator with given name
*/
getStoreGenerator(name: string): StoreGenerator {
return storeGeneratorRegistry.get(name);
},
/**
* Internally used function to get the store creator that was passed to `register`.
* @param name
* @returns storeCreator with given name
*/
export const getStoreGenerator = (name: string): StoreGenerator => storeGeneratorRegistry.get(name);

/**
* Internally used function to set the hydrated store after a Rails page is loaded.
* @param name
* @param store (not the storeGenerator, but the hydrated store)
*/
setStore(name: string, store: Store): void {
hydratedStoreRegistry.set(name, store);
},
/**
* Internally used function to set the hydrated store after a Rails page is loaded.
* @param name
* @param store (not the storeGenerator, but the hydrated store)
*/
export function setStore(name: string, store: Store): void {
hydratedStoreRegistry.set(name, store);
}

/**
* Internally used function to completely clear hydratedStores Map.
*/
clearHydratedStores(): void {
hydratedStoreRegistry.clear();
},
/**
* Internally used function to completely clear hydratedStores Map.
*/
export function clearHydratedStores(): void {
hydratedStoreRegistry.clear();
}

/**
* Get a Map containing all registered store generators. Useful for debugging.
* @returns Map where key is the component name and values are the store generators.
*/
storeGenerators(): Map<string, StoreGenerator> {
return storeGeneratorRegistry.getAll();
},
/**
* Get a Map containing all registered store generators. Useful for debugging.
* @returns Map where key is the component name and values are the store generators.
*/
export const storeGenerators = (): Map<string, StoreGenerator> => storeGeneratorRegistry.getAll();

/**
* Get a Map containing all hydrated stores. Useful for debugging.
* @returns Map where key is the component name and values are the hydrated stores.
*/
stores(): Map<string, Store> {
return hydratedStoreRegistry.getAll();
},
/**
* Get a Map containing all hydrated stores. Useful for debugging.
* @returns Map where key is the component name and values are the hydrated stores.
*/
export const stores = (): Map<string, Store> => hydratedStoreRegistry.getAll();

/**
* Used by components to get the hydrated store, waiting for it to be hydrated if necessary.
* @param name Name of the store to wait for
* @returns Promise that resolves with the Store once hydrated
*/
getOrWaitForStore(name: string): Promise<Store> {
return hydratedStoreRegistry.getOrWaitForItem(name);
},
/**
* Used by components to get the hydrated store, waiting for it to be hydrated if necessary.
* @param name Name of the store to wait for
* @returns Promise that resolves with the Store once hydrated
*/
export const getOrWaitForStore = (name: string): Promise<Store> =>
hydratedStoreRegistry.getOrWaitForItem(name);

/**
* Used by components to get the store generator, waiting for it to be registered if necessary.
* @param name Name of the store generator to wait for
* @returns Promise that resolves with the StoreGenerator once registered
*/
getOrWaitForStoreGenerator(name: string): Promise<StoreGenerator> {
return storeGeneratorRegistry.getOrWaitForItem(name);
},
};
/**
* Used by components to get the store generator, waiting for it to be registered if necessary.
* @param name Name of the store generator to wait for
* @returns Promise that resolves with the StoreGenerator once registered
*/
export const getOrWaitForStoreGenerator = (name: string): Promise<StoreGenerator> =>
storeGeneratorRegistry.getOrWaitForItem(name);
7 changes: 2 additions & 5 deletions node_package/src/buildConsoleReplay.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import RenderUtils from './RenderUtils';
import { wrapInScriptTags } from './RenderUtils';
import scriptSanitizedVal from './scriptSanitizedVal';

declare global {
Expand Down Expand Up @@ -54,8 +54,5 @@ export default function buildConsoleReplay(
customConsoleHistory: (typeof console)['history'] | undefined = undefined,
numberOfMessagesToSkip: number = 0,
): string {
return RenderUtils.wrapInScriptTags(
'consoleReplayLog',
consoleReplay(customConsoleHistory, numberOfMessagesToSkip),
);
return wrapInScriptTags('consoleReplayLog', consoleReplay(customConsoleHistory, numberOfMessagesToSkip));
}
7 changes: 2 additions & 5 deletions node_package/src/serverRenderReactComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';
import * as ReactDOMServer from 'react-dom/server';
import type { ReactElement } from 'react';

import ComponentRegistry from './ComponentRegistry';
import * as ComponentRegistry from './ComponentRegistry';
import createReactOutput from './createReactOutput';
import { isPromise, isServerRenderHash } from './isServerRenderResult';
import buildConsoleReplay from './buildConsoleReplay';
Expand Down Expand Up @@ -144,10 +144,7 @@ function serverRenderReactComponentInternal(options: RenderParams): null | strin
throwJsErrors,
} = options;

let renderState: RenderState = {
result: null,
hasErrors: false,
};
let renderState: RenderState;

try {
const componentObj = ComponentRegistry.get(componentName);
Expand Down
Loading
Loading