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

feat: support SSR #166

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ node_modules

# Artifacts
dist

examples/**/package-lock.json
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,11 @@ With this `{rootDir}/src/ui/tsconfig.json`:
- `forkTsCheker` (`false | ForkTsCheckerWebpackPluginOptions`) - config for ForkTsCheckerWebpackPlugin [more](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin#options). If `false`, ForkTsCheckerWebpackPlugin will be disabled.
- `cache` (`boolean | FileCacheOptions | MemoryCacheOptions`) — Cache the generated webpack modules and chunks to improve build speed. [more](https://webpack.js.org/configuration/cache/)
- `babelCacheDirectory` (`boolean | string`) — Set directory for babel-loader cache (`default: node_modules/.cache/babel-loader``)
- `babel` (`(config: babel.TransformOptions, options: {configType: 'development' | 'production'}) => babel.TransformOptions | Promise<babel.TransformOptions>`) - Allow override the default babel transform options.
- `webpack` (`(config: webpack.Configuration, options: {configType: 'development' | 'production'}) => webpack.Configuration | Promise<webpack.Configuration>`) - Allow override the default configuration.
- `babel` (`(config: babel.TransformOptions, options: {configType: 'development' | 'production'; isSsr: boolean}) => babel.TransformOptions | Promise<babel.TransformOptions>`) - Allow override the default babel transform options.
- `webpack` (`(config: webpack.Configuration, options: {configType: 'development' | 'production'; isSsr: boolean}) => webpack.Configuration | Promise<webpack.Configuration>`) - Allow override the default configuration.
- `ssr` - build SSR bundle. The SSR entries should be inside `src/ui/ssr` directory and match the client entries.
- `noExternal` (`string | RegExp | (string | RegExp)[] | true`) - prevent listed dependencies from being externalized for SSR. By default, all dependencies are externalized.
- `moduleType`: (`'commonjs' | 'esm'`) - library type for the SSR bundle, by default `commonjs`.

##### Dev build

Expand Down
2 changes: 2 additions & 0 deletions examples/ssr/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
dist/
4 changes: 4 additions & 0 deletions examples/ssr/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": ["@gravity-ui/eslint-config", "@gravity-ui/eslint-config/import-order", "@gravity-ui/eslint-config/prettier"],
"root": true
}
2 changes: 2 additions & 0 deletions examples/ssr/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
dist
1 change: 1 addition & 0 deletions examples/ssr/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
2 changes: 2 additions & 0 deletions examples/ssr/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist
package-lock.json
1 change: 1 addition & 0 deletions examples/ssr/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"@gravity-ui/prettier-config"
7 changes: 7 additions & 0 deletions examples/ssr/.stylelintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": [
"@gravity-ui/stylelint-config",
"@gravity-ui/stylelint-config/order",
"@gravity-ui/stylelint-config/prettier"
]
}
1 change: 1 addition & 0 deletions examples/ssr/.tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nodejs 22.12.0
32 changes: 32 additions & 0 deletions examples/ssr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# gravity-ui-app-builder-example

App-builder template with: typescript, sass and Gravity UI.

## ⚗️ Technologies list

- [Gravity UI](https://gravity-ui.com/)
- [TypeScript](https://www.typescriptlang.org/)
- [Sass](https://sass-lang.com/)
- [ESlint](https://eslint.org/) & [Prettier](https://prettier.io/)

## 🚀 Start using it

To use this template for your app you can run:

```sh
npx degit gravity-ui/app-builder/examples/ssr#main my-project
cd my-project

npm install
npm run dev
```

or

```sh
pnpx degit gravity-ui/app-builder/examples/ssr#main my-project
cd my-project

pnpm install
pnpm run dev
```
18 changes: 18 additions & 0 deletions examples/ssr/app-builder.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {defineConfig} from '@gravity-ui/app-builder';

export default defineConfig(() => {
return {
client: {
devServer: {
port: true,
},
ssr: {
noExternal: [/@gravity-ui\/.+/], // dependencies with ccs imports in sources or not correct esm modules should be bundled.
moduleType: 'esm', // default commonjs
},
},
server: {
port: true,
},
};
});
44 changes: 44 additions & 0 deletions examples/ssr/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "ssr",
"version": "1.0.0",
"description": "",
"type": "module",
"scripts": {
"dev": "app-builder dev",
"build": "app-builder build",
"start": "NODE_ENV=production node dist/server",
"lint": "npm run lint:code && npm run lint:styles",
"lint:code": "eslint --ext .js,.jsx,.ts,.tsx .",
"lint:styles": "stylelint \"src/ui/**/*.scss\""
},
"keywords": [],
"license": "MIT",
"devDependencies": {
"@gravity-ui/app-builder": "*",
"@gravity-ui/eslint-config": "^3.2.0",
"@gravity-ui/prettier-config": "^1.1.0",
"@gravity-ui/stylelint-config": "^4.0.1",
"@gravity-ui/tsconfig": "^1.0.0",
"@types/cookie-parser": "^1.4.8",
"@types/express": "^5",
"@types/node": "^22",
"@types/react": "^18.3.2",
"@types/react-dom": "^18.3.0",
"eslint": "^8",
"prettier": "^3.2.5",
"stylelint": "^15",
"typescript": "~5.6.3"
},
"dependencies": {
"@babel/runtime": "^7.26.0",
"@bem-react/classname": "^1.6.0",
"@gravity-ui/icons": "^2.11.0",
"@gravity-ui/uikit": "^6.37.0",
"cookie-parser": "^1.4.7",
"express": "^5",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router": "^7.0.2",
"zustand": "^5.0.2"
}
}
3 changes: 3 additions & 0 deletions examples/ssr/src/server/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["@gravity-ui/eslint-config/server"]
}
60 changes: 60 additions & 0 deletions examples/ssr/src/server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {randomUUID} from 'node:crypto';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as url from 'node:url';

import cookieParser from 'cookie-parser';
import express from 'express';
import * as ReactDOM from 'react-dom/server';

const __dirname = url.fileURLToPath(new URL('.', import.meta.url));

const manifest = JSON.parse(
fs.readFileSync(path.join(__dirname, '../public/build/assets-manifest.json'), 'utf-8'),
);
const ssrManifest = JSON.parse(
fs.readFileSync(path.join(__dirname, '../ssr/assets-manifest.json'), 'utf-8'),
);

const cssAssets = manifest.entrypoints.main.assets.css ?? [];
const links = cssAssets.map((v: string) => ({rel: 'stylesheet', href: `/build/${v}`}));

const app = express();

app.use(cookieParser());
app.use('/build', express.static(path.join(__dirname, '../public/build')));

const isProduction = process.env.NODE_ENV === 'production';

app.get('*all', async (req, res) => {
const {render} = await import(
'../ssr/' + ssrManifest['main.mjs'] + (isProduction ? '' : `?q=${randomUUID()}`)
);
const theme = getUserTheme(req);
const {pipe} = ReactDOM.renderToPipeableStream(render({links, theme, url: req.url}), {
bootstrapScripts: manifest.entrypoints.main.assets.js.map((v: string) => `/build/${v}`),
bootstrapScriptContent: `window.links = ${JSON.stringify(links)};`,
onShellReady() {
res.setHeader('content-type', 'text/html');
pipe(res);
},
});
});

const port = Number(process.env.APP_PORT || 3015);

app.listen(port, () => {
console.log(`Server is running on port ${port}`);

Check warning on line 47 in examples/ssr/src/server/index.ts

View workflow job for this annotation

GitHub Actions / Verify Files

Unexpected console statement
});

function getUserTheme(req: express.Request) {
const cookie = req.cookies['theme-storage'];
try {
const {
state: {theme},
} = JSON.parse(cookie);
return theme;
} catch {
return 'light';
}
}
8 changes: 8 additions & 0 deletions examples/ssr/src/server/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/server"
},
"include": ["**/*"],
"exclude": []
}
8 changes: 8 additions & 0 deletions examples/ssr/src/ui/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "@gravity-ui/eslint-config/client",
"rules": {
"react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off",
"valid-jsdoc": "off"
}
}
1 change: 1 addition & 0 deletions examples/ssr/src/ui/assets/gravity-logo-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions examples/ssr/src/ui/assets/gravity-logo-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions examples/ssr/src/ui/assets/icons/figma.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions examples/ssr/src/ui/assets/icons/github.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions examples/ssr/src/ui/assets/icons/storybook.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions examples/ssr/src/ui/containers/About/About.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {Text} from '@gravity-ui/uikit';

export function About() {
return (
<div
style={{display: 'grid', alignItems: 'center', justifyItems: 'center', height: '100%'}}
>
<Text variant="display-4">About</Text>
</div>
);
}
9 changes: 9 additions & 0 deletions examples/ssr/src/ui/containers/App/App.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
$block: '.app';

#{$block} {
&__theme-button {
position: fixed;
inset-block-start: 0;
inset-inline-end: 0;
}
}
Loading
Loading