├── .npmignore ├── test ├── public │ ├── index.js │ ├── index.css │ ├── nested │ │ ├── index.css │ │ └── index.html │ ├── nested.dot │ │ ├── index.dot.css │ │ └── index.html │ ├── iframe.html │ ├── index.html │ └── page.html ├── tsconfig.json ├── test-request.js ├── docker │ ├── nginx.Dockerfile │ ├── docker-compose.yml │ ├── ssr-proxy-js.config.json │ ├── ssr.Dockerfile │ └── nginx.conf ├── ssr-proxy-js.config.json ├── ssr-build-js.config.json ├── test-stream.js ├── proxy.ts ├── package.json ├── build.ts └── proxy.js ├── src ├── index.ts ├── plugins.ts ├── proxy-cache.ts ├── logger.ts ├── utils.ts ├── ssr-render.ts ├── ssr-build.ts ├── types.ts └── ssr-proxy.ts ├── .dockerignore ├── .gitignore ├── webpack.config.js ├── package.json ├── bin └── cli.js ├── tsconfig.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | test/ -------------------------------------------------------------------------------- /test/public/index.js: -------------------------------------------------------------------------------- 1 | document.body.innerHTML = `\n\t
JS Imported!
${document.body.innerHTML}`; -------------------------------------------------------------------------------- /test/public/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #222222; 3 | } 4 | 5 | p { 6 | margin: 50px; 7 | color: #f5f5f5; 8 | } -------------------------------------------------------------------------------- /test/public/nested/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #777777 !important; 3 | } 4 | 5 | p { 6 | margin: 50px; 7 | color: #f5f5f5; 8 | } -------------------------------------------------------------------------------- /test/public/nested.dot/index.dot.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #777777 !important; 3 | } 4 | 5 | p { 6 | margin: 50px; 7 | color: #f5f5f5; 8 | } -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "esnext" 5 | }, 6 | "exclude": [ "node_modules" ] 7 | } -------------------------------------------------------------------------------- /test/test-request.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | axios.get('http://localhost:3000/login') 4 | .then(r => r.data) 5 | .catch(err => err.response ? err.response.data : err.toString()) 6 | .then(console.log); -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './logger'; 2 | export * from './plugins'; 3 | export * from './proxy-cache'; 4 | export * from './ssr-build'; 5 | export * from './ssr-proxy'; 6 | export * from './ssr-render'; 7 | export * from './types'; 8 | export * from './utils'; 9 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Node.js .dockerignore 2 | 3 | **/dist 4 | **/logs 5 | **/*.log 6 | **/node_modules/ 7 | **/npm-debug.log 8 | **/.git 9 | **/.vscode 10 | **/.gitignore 11 | **/.dockerignore 12 | **/README.md 13 | **/LICENSE 14 | **/.editorconfig 15 | **/Dockerfile 16 | **/*.Dockerfile 17 | **/docs 18 | **/.github -------------------------------------------------------------------------------- /test/docker/nginx.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.27.0-bookworm 2 | 3 | WORKDIR /app 4 | 5 | RUN apt-get update 6 | RUN apt-get install -y net-tools nano 7 | RUN rm -rf /var/lib/apt/lists/* 8 | 9 | COPY ./public/ ./public/ 10 | COPY ./docker/nginx.conf /etc/nginx/ 11 | 12 | EXPOSE 8080 13 | 14 | CMD nginx -g 'daemon off;' 15 | -------------------------------------------------------------------------------- /test/public/iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |12 | Ipsum tempor non amet amet tempor fugiat consequat voluptate Lorem veniam culpa. Nostrud et voluptate ea excepteur veniam exercitation. Reprehenderit nulla irure tempor laborum ut velit. 13 |
14 | 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js .gitignore 2 | 3 | **/dist 4 | **/node_modules 5 | output.html 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | pnpm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | lerna-debug.log* 15 | 16 | # OS 17 | .DS_Store 18 | 19 | # Tests 20 | /coverage 21 | /.nyc_output 22 | 23 | # IDEs and editors 24 | /.idea 25 | .project 26 | .classpath 27 | .c9/ 28 | *.launch 29 | .settings/ 30 | *.sublime-workspace 31 | 32 | # IDE - VSCode 33 | .vscode/* 34 | !.vscode/settings.json 35 | !.vscode/tasks.json 36 | !.vscode/launch.json 37 | !.vscode/extensions.json -------------------------------------------------------------------------------- /test/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | web-server: 3 | container_name: web-server 4 | image: web-server 5 | restart: always 6 | build: 7 | context: ../ 8 | dockerfile: ./docker/nginx.Dockerfile 9 | ports: 10 | - 8080:8080 11 | networks: 12 | - default 13 | depends_on: 14 | - ssr-proxy 15 | 16 | ssr-proxy: 17 | container_name: ssr-proxy 18 | image: ssr-proxy 19 | restart: always 20 | build: 21 | context: ../../ 22 | dockerfile: ./test/docker/ssr.Dockerfile 23 | ports: 24 | - 8081:8081 25 | networks: 26 | - default 27 | 28 | networks: 29 | default: 30 | driver: bridge -------------------------------------------------------------------------------- /test/ssr-proxy-js.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "httpPort": 8081, 3 | "hostname": "0.0.0.0", 4 | "targetRoute": "https://react.dev", 5 | "proxyOrder": ["SsrProxy","HttpProxy"], 6 | "isBot": true, 7 | "ssr": { 8 | "browserConfig": { 9 | "headless": true, 10 | "args": ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"] 11 | }, 12 | "allowedResources": ["document", "script", "xhr", "fetch"], 13 | "waitUntil": "networkidle0" 14 | }, 15 | "httpProxy": { 16 | "shouldUse": true, 17 | "unsafeHttps": true 18 | }, 19 | "static": { 20 | "shouldUse": false 21 | }, 22 | "log": { 23 | "level": 2, 24 | "console": { 25 | "enabled": true 26 | }, 27 | "file": { 28 | "enabled": false 29 | } 30 | }, 31 | "cache": { 32 | "shouldUse": false 33 | } 34 | } -------------------------------------------------------------------------------- /test/ssr-build-js.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "httpPort": 8080, 3 | "hostname": "localhost", 4 | "src": "public", 5 | "dist": "dist", 6 | "ssr": { 7 | "browserConfig": { "headless": true, "args": ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"], "timeout": 60000 }, 8 | "sharedBrowser": false, 9 | "queryParams": [{ "key": "headless", "value": "true" }], 10 | "allowedResources": ["document", "script", "xhr", "fetch"], 11 | "waitUntil": "networkidle0", 12 | "timeout": 60000 13 | }, 14 | "log": { 15 | "level": 2, 16 | "console": { 17 | "enabled": true 18 | }, 19 | "file": { 20 | "enabled": false 21 | } 22 | }, 23 | "job": { 24 | "retries": 3, 25 | "parallelism": 5, 26 | "routes": [ 27 | { "method": "GET", "url": "/" }, 28 | { "method": "GET", "url": "/nested" }, 29 | { "method": "GET", "url": "/page.html" }, 30 | { "method": "GET", "url": "/iframe.html" } 31 | ] 32 | } 33 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pkg = require('./package.json'); 3 | const nodeExternals = require('webpack-node-externals'); 4 | 5 | module.exports = { 6 | // mode: 'production', 7 | target: 'node', // in order to ignore built-in modules like path, fs, etc. 8 | entry: path.resolve(__dirname, 'src/index.ts'), 9 | module: { 10 | rules: [{ 11 | test: /\.ts$/, 12 | include: /src/, 13 | use: [{ 14 | loader: 'ts-loader' , 15 | options: { configFile: 'tsconfig.json' }, 16 | }], 17 | }], 18 | }, 19 | resolve: { 20 | extensions: ['.ts', '.js'], 21 | }, 22 | output: { 23 | library: pkg.name, 24 | libraryTarget: 'umd', 25 | filename: 'index.js', 26 | path: path.resolve(__dirname, 'dist'), 27 | }, 28 | externals: [ 29 | nodeExternals(), // in order to ignore all modules in node_modules folder 30 | ], 31 | }; -------------------------------------------------------------------------------- /test/test-stream.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const Transform = require('stream').Transform; 3 | 4 | const parser = new Transform(); 5 | parser._transform = (chunk, encoding, callback) => { 6 | const str = chunk.toString(); 7 | const error = null; // new Error('test'); 8 | 9 | console.log('\n--- CHUNK ---\n', str, '\n', error); 10 | 11 | callback(error, str); 12 | }; 13 | 14 | console.log('\n--- BEGIN STREAM ---'); 15 | 16 | // Create and Transform Stream 17 | let stream = fs.createReadStream('../../../build/index.html'); // Create 18 | stream = stream.pipe(parser); // Transform 19 | stream = stream.on('end', () => console.log('\n--- END STREAM ---')); // Runs after all data is read 20 | 21 | // Read Stream 22 | streamToString(stream).then(str => console.log('\n--- FULL DATA ---\n', str)).catch(e => e); 23 | 24 | 25 | 26 | function streamToString(stream) { 27 | const chunks = []; 28 | return new Promise((res, rej) => { 29 | if (!stream?.on) return res(stream); 30 | stream.on('data', chunk => chunks.push(Buffer.from(chunk))); 31 | stream.on('error', err => rej(err)); 32 | stream.on('end', () => res(Buffer.concat(chunks).toString('utf8'))); 33 | }); 34 | } -------------------------------------------------------------------------------- /src/plugins.ts: -------------------------------------------------------------------------------- 1 | // https://rollupjs.org/plugin-development/#writebundle 2 | // https://vite.dev/guide/api-plugin 3 | 4 | import { SsrBuild } from './ssr-build'; 5 | import { SsrBuildConfig } from './types'; 6 | 7 | type Apply = 'serve' | 'build'; 8 | type Enforce = 'pre' | 'post' | undefined; 9 | type Order = 'pre' | 'post' | undefined; 10 | type Event = 'writeBundle' | 'buildEnd' | 'closeBundle' | (string & {}); 11 | 12 | export const ssrBuildVitePlugin = (config: SsrBuildConfig, pluginOverride?: { apply?: Apply, enforce?: Enforce, [key: string]: any; }) => { 13 | return { 14 | name: 'ssr-build-js', 15 | apply: 'build' as Apply, 16 | // enforce: 'pre' as Enforce, 17 | writeBundle: { 18 | sequential: true, 19 | // order: 'pre' as Order, 20 | async handler(outputOptions: any, bundle: any) { 21 | const ssrBuild = new SsrBuild(config); 22 | const result = await ssrBuild.start(); 23 | result.forEach(e => { 24 | const fileName = e.urlPath.replace(/^\/+/, ''); 25 | const duplicate = Object.keys(bundle).find(e => e === fileName); 26 | if (duplicate) delete bundle[duplicate]; 27 | (this as any).emitFile({ type: 'asset', fileName, source: e.text }); 28 | }); 29 | }, 30 | }, 31 | ...(pluginOverride || {}), 32 | }; 33 | }; -------------------------------------------------------------------------------- /test/proxy.ts: -------------------------------------------------------------------------------- 1 | // Run "npm run serve" in parallel 2 | 3 | import { LogLevel, SsrProxy, SsrProxyConfig } from 'ssr-proxy-js-local'; // ssr-proxy-js or ssr-proxy-js-local 4 | 5 | const BASE_PROXY_PORT = '8080'; 6 | const BASE_PROXY_ROUTE = `http://localhost:${BASE_PROXY_PORT}`; 7 | 8 | // Proxy 9 | 10 | const config: SsrProxyConfig = { 11 | httpPort: 8081, 12 | hostname: '0.0.0.0', 13 | targetRoute: BASE_PROXY_ROUTE, 14 | isBot: true, 15 | reqMiddleware: async (params) => { 16 | params.targetUrl.search = ''; 17 | params.targetUrl.pathname = params.targetUrl.pathname.replace(/\/+$/, '') || '/'; 18 | return params; 19 | }, 20 | resMiddleware: async (params, result) => { 21 | if (result.text == null) return result; 22 | result.text = result.text.replace('