
Research
/Security News
11 Malicious NuGet Tools Pose as Game Cheats to Drop a Windows Host-Surveillance Payload
11 malicious NuGet tools pose as game cheats to deploy Windows payloads, track hosts, and use Google Sheets for telemetry and control.
@ecopages/react
Advanced tools
First-class integration for React 19 in Ecopages. This plugin enables React SSR and client hydration, allowing you to build component-level React islands or full React Single Page Applications (SPAs).
bun add @ecopages/react react react-dom
bun add -d @types/react @types/react-dom
Configure the plugin in your eco.config.ts:
import { ConfigBuilder } from '@ecopages/core/config-builder';
import { reactPlugin } from '@ecopages/react';
const config = await new ConfigBuilder()
.setBaseUrl(import.meta.env.ECOPAGES_BASE_URL)
.setIntegrations([reactPlugin()])
.build();
export default config;
For component-level islands, Ecopages React uses this contract:
data-eco-component-id attribute is attached to the component SSR root.createRoot(). Full-page hydration paths use hydrateRoot().[!TIP] Full React SPA Routing: If you are building full React pages and want client-side navigation (SPA), use @ecopages/react-router and pass it to the react plugin:
reactPlugin({ router: ecoRouter() }).
The React plugin includes built-in MDX support. When enabled, you can write .mdx pages alongside .tsx pages with unified client-side routing, hydration, and HMR.
import { ConfigBuilder } from '@ecopages/core/config-builder';
import { reactPlugin } from '@ecopages/react';
const config = await new ConfigBuilder()
.setIntegrations([
reactPlugin({
mdx: {
enabled: true,
compilerOptions: {
// Optional: remark/rehype plugins
},
},
}),
])
.build();
export default config;
The React integration can participate in mixed-renderer apps in three ways:
When a non-React render pass reaches a React-owned foreign child, Ecopages hands that foreign subtree back to the React renderer. When React renders through a non-React shell, that shell must serialize to HTML so React can insert the result into the final response without escaping it.
Important:
config.dependencies.components.The React integration supports Node.js modules and server-only code only on the server execution graph.
node:* modules, database clients, filesystem utilities, etc.Keep server helpers close, but separate them physically or logically so they do not leak into the client bundle.
This section explains the internal contract used to keep the browser bundle minimal while preventing server-only code and request-only configuration from leaking into client output.
The React integration has two jobs that must hold at the same time:
That means the client bundle must keep client-safe render logic, but it must drop server-only imports and server-only eco.page(...) options such as middleware and build-time metadata.
Think about each React page as two related graphs:
The React integration builds the client graph conservatively. If a server-only module becomes reachable from the hydrated render path, the build should fail rather than silently shipping unsafe code.
The client bundle keeps:
The client bundle removes or excludes:
eco.page(...) options such as cache, middleware, metadata, staticProps, and staticPaths.Important:
render must stay in the client bundle, because hydration needs it to reconstruct the page tree.requires does not stay in the browser page config. It is used on the server to decide which locals keys may be serialized into the hydration payload.The browser-bound transform in src/utils/client-graph-boundary-plugin.ts follows this order:
eco.page(...) object properties from the reparsed AST.The reparse step is important. Once import edits change source offsets, the original AST locations are stale. Reusing them for later edits can corrupt the output or remove the wrong code.
eco.page(...) Options Are StrippedImport pruning alone is not enough.
Consider a page like this:
import { authMiddleware } from './auth.server';
export default eco.page({
cache: 'dynamic',
middleware: [authMiddleware],
requires: ['session'] as const,
render: () => <div>Dashboard</div>,
});
If the client transform removes the auth.server import but leaves middleware: [authMiddleware] in place, the browser bundle still contains a dangling identifier. That breaks production hydration even though the import was removed correctly.
The fix is to strip server-only eco.page(...) options after import pruning, while keeping render intact.
localsThe browser must not receive arbitrary request-scoped data.
The React renderer in src/react-renderer.ts serializes only the top-level locals keys explicitly declared by Page.requires. If a page does not declare requires, no locals are serialized for hydration.
Example:
export default eco.page({
requires: ['session'] as const,
render: ({ locals }) => <Dashboard user={locals?.session?.user} />,
});
In this case, the hydration payload may include locals.session, but it will exclude unrelated request-only keys.
Important:
locals.session itself contains sensitive nested fields, those fields will still be serialized.requires.Hydration must rebuild the same tree the server rendered.
That applies to both:
If the page render receives locals on the server and the layout also depends on those values, the client must pass the same serialized locals into the layout during hydration. Otherwise React will detect a mismatch.
The main regression coverage lives in:
eco.page(...) options are stripped from browser bundles.requires keys are serialized into hydration payloads.locals into layouts.locals with persistLayouts both enabled and disabled.If you change the AST transform or hydration flow, update the corresponding tests in the same change.
Pages may declare layout as a single component or an outer → inner array on eco.page(). On the server, React-managed layout stacks compose through composeDocumentShell + composeLayoutPageTree so provider context reaches nested pages during SSR. On the client, @ecopages/react-router PageContent uses the same composeLayoutPageTree helper unless persistLayouts is enabled (then each tier is cached independently).
During SSR, ReactRenderer passes the app-resolved React runtime into composeLayoutPageTree via options.react so layout trees are built with the same module instance as renderToString. App code should import hooks and context from react normally; keep a single React version in the app dependency graph (avoid duplicate react copies in monorepos).
Layout prop factories receive LayoutPropsContext (params, query, locals). Route-scoped locals are passed to layout tiers via document-shell props during SSR; serialized pageProps.locals follow Page.requires and are intended for the page component.
composeLayoutPageTree.With ecoRouter(), Ecopages keeps outer layout tiers mounted across client navigation (persistLayouts defaults to true). Each page chunk still loads separately, but provider layouts stay alive in the DOM.
That creates a module identity problem: if TanStack Query (or any shared provider library) is bundled into every page chunk separately, each chunk gets its own copy of the library and its own React context. Navigation then breaks with errors such as No QueryClient set, even though the provider component is still mounted.
The fix is shared browser runtime vendors: selected npm packages are built once into /assets/vendors/*.js and every page chunk imports the same public URL. React, React DOM, the router bundle, and auto-discovered layout runtime packages all follow this path.
Auto-discovery removes the need to hand-maintain runtimeModules for the common case — a query-client layout that imports @tanstack/react-query is enough when discovery is configured correctly.
Auto-discovery runs at plugin setup when both are true:
router is passed to reactPlugin()absolutePaths.projectDir, layoutsDir, and componentsDirWithout router, only explicit runtimeModules entries are vendored.
| Mode | Trigger | Layout roots scanned | npm packages collected |
|---|---|---|---|
| Runtime-provider (recommended) | At least one layout sets runtimeProvider: true | Only flagged layouts | Every reachable npm package in that layout's render graph |
| Provider-scoped fallback | No layout sets runtimeProvider: true | All eco.layout( files under layouts/ and components/ | npm packages imported from provider/context modules only |
The fallback exists for backward compatibility. Ecopages logs a debug message when fallback mode is active. Prefer explicit runtimeProvider: true on provider root layouts (for example a query-client tier) and omit it from shell-only layouts.
render client graph (reachability analysis), follow relative imports and tsconfig path aliases. Type-only imports and .server.ts modules are skipped.@tanstack/react-query, not @tanstack/react-query/devtools).Excluded automatically: React, React DOM, jsx runtimes, the router bundle, @ecopages/*, workspace packages under @techn.es/*, *-devtools packages, and packages already vendored by the React plugin.
Path aliases resolve from tsconfig.json compilerOptions.paths (oxc-resolver), same as the Ecopages alias resolver plugin.
Plugin config — no manual vendor list when provider layouts are flagged:
import { ConfigBuilder } from '@ecopages/core/config-builder';
import { reactPlugin } from '@ecopages/react';
import { ecoRouter } from '@ecopages/react-router';
const config = await new ConfigBuilder().setIntegrations([reactPlugin({ router: ecoRouter() })]).build();
export default config;
Provider root layout — set runtimeProvider: true on the tier that mounts shared client state:
import type { ReactNode } from 'react';
import { eco } from '@ecopages/core';
import { QueryProvider } from '@/shared/query/query-provider';
export const QueryRootLayout = eco.layout<ReactNode>({
runtimeProvider: true,
render: ({ children }) => <QueryProvider>{children}</QueryProvider>,
});
Shell layouts that do not mount shared runtime state omit the flag:
export const AppShellLayout = eco.layout({
render: ({ children }) => <AppShell>{children}</AppShell>,
});
Stack provider roots before shell tiers in page layout arrays so context wraps the shell on both SSR and persisted client navigation.
Requires matching tsconfig paths when using aliases:
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
Use explicit runtimeModules when discovery misses a package, when router is not enabled, or when you need custom vendor output names or externals:
reactPlugin({
router: ecoRouter(),
runtimeModules: ['@tanstack/react-query', { specifier: '@acme/ui', outputName: 'acme-ui', externals: ['react'] }],
});
Manual entries override auto-discovered entries for the same specifier.
| Symptom | Likely cause | Fix |
|---|---|---|
No QueryClient set after SPA navigation | Provider library bundled per page chunk | Ensure router is enabled; add runtimeProvider: true on the provider layout; rebuild vendors |
| Wrong packages vendored (slow dev startup) | Shell layout scanned as discovery root | Set runtimeProvider: true only on provider roots; keep shell layouts unflagged |
| Package not discovered | Layout outside layouts/ / components/, or import not reachable from render | Move layout file or add explicit runtimeModules entry |
@/ alias not followed | Missing or invalid tsconfig paths | Add compilerOptions.paths; ensure include globs are valid JSON (not broken by comment stripping) |
Pages and layouts SSR through renderToString, which does not support <Suspense>. Do not use React.lazy() + <Suspense> in eco.page() or eco.layout() trees.
Wrap browser-only UI in ClientOnly:
import { eco } from '@ecopages/core';
import { ClientOnly } from '@ecopages/react/utils/client-only';
export const RootLayout = eco.layout({
render: ({ children }) => (
<>
{children}
<ClientOnly fallback={null}>
<DevtoolsPanel />
</ClientOnly>
</>
),
});
For code-split client-only modules, import() inside useEffect within ClientOnly — not lazy(). dynamic({ ssr: false }) must also stay inside ClientOnly; it renders null on the server and lazy() in the browser.
FAQs
React integration for Ecopages
The npm package @ecopages/react receives a total of 160 weekly downloads. As such, @ecopages/react popularity was classified as not popular.
We found that @ecopages/react demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
/Security News
11 malicious NuGet tools pose as game cheats to deploy Windows payloads, track hosts, and use Google Sheets for telemetry and control.

Research
/Security News
4 compromised asyncapi packages deliver miasma botnet loader on macOS, Linux and Windows.

Research
/Security News
A compromised jscrambler npm release added a malicious preinstall hook that runs hidden native binaries on Linux, macOS, and Windows.