Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PoC - Vite and Tanstack Router, Query and @plone/client in SSR mode. #5750

Merged
merged 15 commits into from
Feb 13, 2024
Merged
2 changes: 1 addition & 1 deletion .github/workflows/readme-link-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ jobs:
- name: Check links in README.md with awesome_bot
run: |
gem install awesome_bot
awesome_bot --request-delay 1 --allow-dupe --white-list http://localhost:8080/Plone,http://localhost:3000,https://github.com/kitconcept/volto-blocks-grid.git --files README.md,packages/client/README.md,packages/components/README.md,packages/generator-volto/README.md,packages/registry/README.md,packages/scripts/README.md,packages/types/README.md,packages/volto-slate/README.md
awesome_bot --request-delay 1 --allow-dupe --white-list http://localhost:8080/Plone,http://localhost:3000,https://github.com/kitconcept/volto-blocks-grid.git --files README.md,packages/client/README.md,packages/components/README.md,packages/generator-volto/README.md,packages/registry/README.md,packages/scripts/README.md,packages/types/README.md,packages/volto-slate/README.md,apps/nextjs/README.md,apps/vite-ssr/README.md
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,10 @@ copyreleasenotestodocs:
start-backend-docker:
docker run -it --rm --name=backend -p 8080:8080 -e SITE=Plone -e ADDONS='$(KGS)' $(DOCKER_IMAGE)

.PHONY: start-backend-docker-no-cors
start-backend-docker-nocors:
docker run -it --rm --name=backend -p 8080:8080 -e SITE=Plone -e ADDONS='$(KGS)' -e CORS_=true $(DOCKER_IMAGE)

.PHONY: start-frontend-docker
start-frontend-docker:
docker run -it --rm --name=volto --link backend -p 3000:3000 -e RAZZLE_INTERNAL_API_PATH=http://backend:8080/Plone -e RAZZLE_DEV_PROXY_API_PATH=http://backend:8080/Plone plone/plone-frontend:latest
Expand Down
5 changes: 5 additions & 0 deletions apps/vite-ssr/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
1 change: 1 addition & 0 deletions apps/vite-ssr/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/routeTree.gen.ts
6 changes: 6 additions & 0 deletions apps/vite-ssr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Plone on Vite with SSR mode

This is a proof of concept of a [Vite](https://vitejs.dev) build, using `@plone/client` and `@plone/components` libraries.
This is intended to serve as both a playground for the development of both packages and as a demo of Plone using Vite built with server side rendering (SSR).

It also uses [TanStack Router](https://tanstack.com/router/latest/docs/framework/react/overview) for its routing library.
12 changes: 12 additions & 0 deletions apps/vite-ssr/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!--app-head-->
</head>
<body>
<div id="root"><!--app-html--></div>
<script defer type="module" src="/src/entry-client.tsx"></script>
</body>
</html>
45 changes: 45 additions & 0 deletions apps/vite-ssr/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"name": "plone-vite-ssr",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "node server",
"build": "npm run build:client && npm run build:server",
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server",
"serve": "NODE_ENV=production node server",
"debug": "node --inspect-brk server"
},
"dependencies": {
"@plone/client": "workspace:*",
"@tanstack/react-query": "5.0.5",
"@tanstack/react-router": "^1.16.0",
"@tanstack/react-router-server": "^1.16.0",
"@tanstack/router-devtools": "^1.16.0",
"@tanstack/router-vite-plugin": "^1.16.1",
"axios": "^1.6.5",
"get-port": "^7.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sirv": "^2.0.4"
},
"devDependencies": {
"@babel/core": "^7.23.7",
"@babel/generator": "^7.23.6",
"@rollup/plugin-babel": "^6.0.4",
"@tanstack/react-query-devtools": "^5.20.1",
"@types/express": "^4.17.21",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@vitejs/plugin-react": "^4",
"compression": "^1.7.4",
"express": "^4.18.2",
"isbot": "^4.3.0",
"node-fetch": "^3.3.2",
"serve-static": "^1.15.0",
"typescript": "^5.3.3",
"vite": "^5.0.12",
"vite-plugin-babel": "^1.2.0"
}
}
100 changes: 100 additions & 0 deletions apps/vite-ssr/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import fs from 'node:fs/promises';
import express from 'express';
import getPort, { portNumbers } from 'get-port';

const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD;

export async function createServer(
root = process.cwd(),
isProd = process.env.NODE_ENV === 'production',
hmrPort,
) {
const app = express();

const prodIndexHtml = isProd
? await fs.readFile('./dist/client/index.html', 'utf-8')
: '';

/**
* @type {import('vite').ViteDevServer}
*/
let vite;
if (!isProd) {
vite = await (
await import('vite')
).createServer({
root,
logLevel: isTest ? 'error' : 'info',
server: {
middlewareMode: true,
watch: {
// During tests we edit the files too fast and sometimes chokidar
// misses change events, so enforce polling for consistency
usePolling: true,
interval: 100,
},
hmr: {
port: hmrPort,
},
},
appType: 'custom',
});
// use vite's connect instance as middleware
app.use(vite.middlewares);
} else {
const sirv = (await import('sirv')).default;
app.use((await import('compression')).default());
app.use('/', sirv('./dist/client', { extensions: [] }));
}

app.use('*', async (req, res) => {
try {
const url = req.originalUrl;

if (url.includes('.')) {
console.warn(`${url} is not valid router path`);
res.status(404);
res.end(`${url} is not valid router path`);
return;
}

// Extract the head from vite's index transformation hook
let viteHead = !isProd
? await vite.transformIndexHtml(
url,
`<html><head></head><body></body></html>`,
)
: prodIndexHtml;

viteHead = viteHead.substring(
viteHead.indexOf('<head>') + 6,
viteHead.indexOf('</head>'),
);

const entry = await (async () => {
if (!isProd) {
return vite.ssrLoadModule('/src/entry-server.tsx');
} else {
return import('./dist/server/entry-server.js');
}
})();

console.log('Rendering: ', url, '...');
entry.render({ req, res, url, head: isProd ? viteHead : '' });
} catch (e) {
!isProd && vite.ssrFixStacktrace(e);
console.log(e.stack);
res.status(500).end(e.stack);
}
});

return { app, vite };
}

if (!isTest) {
createServer().then(async ({ app }) =>
app.listen(await getPort({ port: portNumbers(3000, 3100) }), () => {
console.log('Client Server: http://localhost:3000');
}),
);
}
9 changes: 9 additions & 0 deletions apps/vite-ssr/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const settings = {
apiPath: 'http://localhost:8080/Plone',
};

const config = {
settings,
};

export default config;
8 changes: 8 additions & 0 deletions apps/vite-ssr/src/entry-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import ReactDOM from 'react-dom/client';

import { StartClient } from '@tanstack/react-router-server/client';
import { createRouter } from './router';

const router = createRouter();

ReactDOM.hydrateRoot(document, <StartClient router={router} />);
44 changes: 44 additions & 0 deletions apps/vite-ssr/src/entry-server.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as React from 'react';
import ReactDOMServer from 'react-dom/server';
import { createMemoryHistory } from '@tanstack/react-router';
import { ServerResponse } from 'http';
import express from 'express';
import { StartServer } from '@tanstack/react-router-server/server';
import { createRouter } from './router';

// index.js
import './fetch-polyfill';

export async function render(opts: {
url: string;
head: string;
req: express.Request;
res: ServerResponse;
}) {
const router = createRouter();

const memoryHistory = createMemoryHistory({
initialEntries: [opts.url],
});

// Update the history and context
router.update({
history: memoryHistory,
context: {
...router.options.context,
head: opts.head,
},
});

// Since we're using renderToString, Wait for the router to finish loading
await router.load();

// Render the app
const appHtml = ReactDOMServer.renderToString(
<StartServer router={router} />,
);

opts.res.statusCode = 200;
opts.res.setHeader('Content-Type', 'text/html');
opts.res.end(`<!DOCTYPE html>${appHtml}`);
}
20 changes: 20 additions & 0 deletions apps/vite-ssr/src/fetch-polyfill.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// fetch-polyfill.js
import fetch, {
Blob,
blobFrom,
blobFromSync,
File,
fileFrom,
fileFromSync,
FormData,
Headers,
Request,
Response,
} from 'node-fetch';

if (!globalThis.fetch) {
globalThis.fetch = fetch;
globalThis.Headers = Headers;
globalThis.Request = Request;
globalThis.Response = Response;
}
93 changes: 93 additions & 0 deletions apps/vite-ssr/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/* prettier-ignore-start */

/* eslint-disable */

// @ts-nocheck

// noinspection JSUnusedGlobalSymbols

// This file is auto-generated by TanStack Router

// Import Routes

import { Route as rootRoute } from './routes/__root';
import { Route as PostsImport } from './routes/posts';
import { Route as ErrorImport } from './routes/error';
import { Route as IndexImport } from './routes/index';
import { Route as PostsIndexImport } from './routes/posts/index';
import { Route as PostsPostIdImport } from './routes/posts/$postId';
import { Route as SplatEditImport } from './routes/$.edit';

// Create/Update Routes

const PostsRoute = PostsImport.update({
path: '/posts',
getParentRoute: () => rootRoute,
} as any);

const ErrorRoute = ErrorImport.update({
path: '/error',
getParentRoute: () => rootRoute,
} as any);

const IndexRoute = IndexImport.update({
path: '/',
getParentRoute: () => rootRoute,
} as any);

const PostsIndexRoute = PostsIndexImport.update({
path: '/',
getParentRoute: () => PostsRoute,
} as any);

const PostsPostIdRoute = PostsPostIdImport.update({
path: '/$postId',
getParentRoute: () => PostsRoute,
} as any);

const SplatEditRoute = SplatEditImport.update({
path: '/$/edit',
getParentRoute: () => rootRoute,
} as any);

// Populate the FileRoutesByPath interface

declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
preLoaderRoute: typeof IndexImport;
parentRoute: typeof rootRoute;
};
'/error': {
preLoaderRoute: typeof ErrorImport;
parentRoute: typeof rootRoute;
};
'/posts': {
preLoaderRoute: typeof PostsImport;
parentRoute: typeof rootRoute;
};
'/$/edit': {
preLoaderRoute: typeof SplatEditImport;
parentRoute: typeof rootRoute;
};
'/posts/$postId': {
preLoaderRoute: typeof PostsPostIdImport;
parentRoute: typeof PostsImport;
};
'/posts/': {
preLoaderRoute: typeof PostsIndexImport;
parentRoute: typeof PostsImport;
};
}
}

// Create and export the route tree

export const routeTree = rootRoute.addChildren([
IndexRoute,
ErrorRoute,
PostsRoute.addChildren([PostsPostIdRoute, PostsIndexRoute]),
SplatEditRoute,
]);

/* prettier-ignore-end */
Loading
Loading