jinrai 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/config/userConfig.ts
4
+ import { createJiti } from "jiti";
5
+ import { pathToFileURL } from "url";
6
+ import { resolve } from "path";
7
+ var getUserConfig = async () => {
8
+ const jiti = createJiti(import.meta.url, {
9
+ debug: false
10
+ });
11
+ const configPath = pathToFileURL(resolve(process.cwd(), ".ssr.config")).href;
12
+ console.log("#######", { configPath });
13
+ const configModule = await jiti.import(configPath);
14
+ return configModule.default;
15
+ };
16
+
17
+ // src/generate.ts
18
+ import ReactDOMServer from "react-dom/server";
19
+ console.log(">>");
20
+ var config = await getUserConfig();
21
+ for (const mask of config.pages) {
22
+ console.log(" - ", mask);
23
+ const url = mask.replaceAll("{", "").replaceAll("}", "");
24
+ const html = ReactDOMServer.renderToString(config.renderPage(url));
25
+ console.log(html);
26
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { defineConfig } from "./src/config/define";
package/lib/index.js ADDED
@@ -0,0 +1,7 @@
1
+ // src/config/define.ts
2
+ var defineConfig = (config) => {
3
+ return config;
4
+ };
5
+ export {
6
+ defineConfig
7
+ };
@@ -0,0 +1,2 @@
1
+ import type { Config } from "./userConfig";
2
+ export declare const defineConfig: (config: Config) => Config;
@@ -0,0 +1,14 @@
1
+ interface ExportHTML {
2
+ index: string;
3
+ outDir: string;
4
+ }
5
+ export interface Config {
6
+ url: string;
7
+ pages: string[];
8
+ api: string;
9
+ dev?: boolean;
10
+ meta: string;
11
+ export: ExportHTML;
12
+ }
13
+ export declare const getUserConfig: () => Promise<Config>;
14
+ export {};
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "jinrai",
3
+ "version": "1.0.0",
4
+ "description": "A powerful library that analyzes your modern web application and automatically generates a perfectly rendered, static snapshot of its pages. Experience unparalleled loading speed and SEO clarity without the complexity of traditional SSR setups. Simply point Jinrai at your SPA and witness divine speed.",
5
+ "main": "lib/index.ts",
6
+ "scripts": {
7
+ "test": "node ./lib/bin",
8
+ "test2": "node --import tsx src/bin.ts",
9
+ "build": "npm run build-config && npm run build-index && npm run build:types",
10
+ "build:types": "tsc",
11
+ "build-index": "npx esbuild index.ts --bundle --platform=node --format=esm --outfile=lib/index.js --external:jiti --external:node:* --external:playwright",
12
+ "build-config": "npx esbuild src/bin.ts --bundle --platform=node --format=esm --outfile=lib/bin.js --external:jiti --external:node:* --external:playwright --external:prettier"
13
+ },
14
+ "keywords": [],
15
+ "author": "",
16
+ "license": "ISC",
17
+ "type": "module",
18
+ "devDependencies": {
19
+ "@types/node": "^24.5.2",
20
+ "@types/ora": "^3.1.0",
21
+ "dotenv": "^17.2.2",
22
+ "tsx": "^4.20.6",
23
+ "typescript": "^5.9.2"
24
+ },
25
+ "bin": {
26
+ "rsr": "lib/bin.js"
27
+ },
28
+ "dependencies": {
29
+ "@types/prettier": "^2.7.3",
30
+ "jiti": "^2.6.0",
31
+ "ora": "^9.0.0",
32
+ "playwright": "^1.55.1",
33
+ "prettier": "^3.6.2"
34
+ }
35
+ }
package/readme.md ADDED
@@ -0,0 +1,24 @@
1
+ # **Jinrai - Instant Static Rendering for SPAs.**
2
+
3
+ A powerful library that analyzes your modern web application and automatically
4
+ generates a perfectly rendered, static snapshot of its pages. Experience unparalleled
5
+ loading speed and SEO clarity without the complexity of traditional SSR setups.
6
+ Simply point Jinrai at your SPA and witness divine speed.
7
+
8
+ ---
9
+
10
+ .ssr.config.ts
11
+
12
+ ```ts
13
+ import { defineConfig } from "jinrai"
14
+
15
+ export default defineConfig({
16
+ url: "<spa-url>",
17
+ dev: true,
18
+ pages: ["/", "/products", "/products/{phones}", "/product/{iphone17pro}", "/{iphone17pro_42}"],
19
+ export: {
20
+ outDir: "export",
21
+ index: "index.html",
22
+ },
23
+ })
24
+ ```
package/src/bin.ts ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { writeFile } from "node:fs/promises"
4
+ import { Config, getUserConfig } from "./config/userConfig"
5
+ import { getRoutesAndTemplates } from "./routes/getRoutes"
6
+ import { getRawPageData } from "./templates"
7
+ import path from "node:path"
8
+ import { mkdir } from "node:fs/promises"
9
+ import prettier from "prettier"
10
+ import Task from "./ui/task"
11
+
12
+ const task = new Task()
13
+
14
+ task.do("Load config")
15
+ const config: Config = await getUserConfig()
16
+ task.success()
17
+
18
+ const data = await getRawPageData(config.url, config.pages, config.dev)
19
+
20
+ task.do("Format")
21
+ const { routes, templates } = getRoutesAndTemplates(data)
22
+
23
+ task.next(`Export: ${config.export.outDir} (${templates.length})`)
24
+ await mkdir(config.export.outDir, { recursive: true })
25
+
26
+ await writeFile(path.join(config.export.outDir, "config.json"), JSON.stringify(routes, null, 2))
27
+
28
+ for await (let [index, template] of templates.entries()) {
29
+ try {
30
+ template = await prettier.format(template, {
31
+ parser: "html",
32
+ printWidth: 500,
33
+ tabWidth: 2,
34
+ useTabs: false,
35
+ semi: true,
36
+ singleQuote: true,
37
+ })
38
+ } catch (error) {}
39
+
40
+ await writeFile(path.join(config.export.outDir, `${index}.html`), template)
41
+ }
42
+
43
+ task.success()
@@ -0,0 +1,7 @@
1
+ import type { Config } from "./userConfig"
2
+
3
+ export const defineConfig = (config: Config) => {
4
+ return config
5
+ }
6
+
7
+
@@ -0,0 +1,34 @@
1
+ import { createJiti } from "jiti"
2
+ import { pathToFileURL } from "url"
3
+ import { resolve } from "path"
4
+
5
+ interface ExportHTML {
6
+ // root: string
7
+ // assets: {
8
+ // root: string
9
+ // prefix: string
10
+ // }
11
+ index: string
12
+ outDir: string
13
+ }
14
+
15
+ export interface Config {
16
+ url: string
17
+ pages: string[]
18
+ api: string
19
+ dev?: boolean
20
+ meta: string
21
+ export: ExportHTML
22
+ }
23
+
24
+ export const getUserConfig = async (): Promise<Config> => {
25
+ const jiti = createJiti(import.meta.url, {
26
+ debug: false,
27
+ })
28
+
29
+ const configPath = pathToFileURL(resolve(process.cwd(), ".ssr.config")).href
30
+
31
+ const configModule = (await jiti.import(configPath)) as { default: Config }
32
+
33
+ return configModule.default
34
+ }
@@ -0,0 +1,29 @@
1
+ import { input, PageData } from "../templates"
2
+ import { ParseItem, Parser } from "./parser"
3
+
4
+ interface Route {
5
+ mask: string
6
+ requests: input[]
7
+ content: ParseItem[]
8
+ }
9
+
10
+ export const getRoutesAndTemplates = (templates: PageData[]) => {
11
+ const routes: Route[] = []
12
+ const parser = new Parser()
13
+
14
+ for (const template of templates) {
15
+ const content = parser.parse(template.root)
16
+ const mask = template.mask.replaceAll("/", "\\/").replace(/{(.*?)}/, ".+?")
17
+
18
+ routes.push({
19
+ content,
20
+ mask,
21
+ requests: template.input,
22
+ })
23
+ }
24
+
25
+ return {
26
+ routes,
27
+ templates: parser.templates,
28
+ }
29
+ }
@@ -0,0 +1,58 @@
1
+ import { splitByTag } from "./splitByTag.ts"
2
+
3
+ export interface ParseItem {
4
+ type: string
5
+ template: ParseItem[] | number
6
+ contentKey?: string
7
+ }
8
+
9
+ const toBinary = (content: string) => {
10
+ return btoa(unescape(encodeURIComponent(content)))
11
+ }
12
+
13
+ export class Parser {
14
+ templates: string[] = []
15
+ ctemplates: string[] = []
16
+ convertor?: (content: string) => string
17
+
18
+ constructor(convertor?: (content: string) => string) {
19
+ this.convertor = convertor
20
+ }
21
+
22
+ parse(html: string): ParseItem[] {
23
+ const parts = splitByTag("loopwrapper", html)
24
+
25
+ return parts.map(content => {
26
+ const isArray = content.startsWith("ArrayDataKey=")
27
+
28
+ let contentKey = undefined
29
+ if (isArray) {
30
+ ;[contentKey, content] = this.firstPypeSplit(content)
31
+ }
32
+
33
+ return {
34
+ type: isArray ? "array" : "html",
35
+ template: isArray ? this.parse(content) : this.getTemplateId(content),
36
+ contentKey,
37
+ }
38
+ })
39
+ }
40
+
41
+ getTemplateId(template: string): number {
42
+ const index = this.templates.indexOf(template)
43
+ if (index != -1) {
44
+ return index
45
+ }
46
+
47
+ this.templates.push(template)
48
+ this.ctemplates.push(this.convertor ? this.convertor(template) : toBinary(template))
49
+
50
+ return this.templates.length - 1
51
+ }
52
+
53
+ firstPypeSplit(content: string) {
54
+ const index = content.indexOf("|")
55
+
56
+ return [content.slice(13, index), content.slice(index + 1)]
57
+ }
58
+ }
@@ -0,0 +1,38 @@
1
+ export const splitByTag = (tag: string, content: string): string[] => {
2
+ const htmls = content
3
+ .replace(new RegExp(`(<\/?${tag}.*?>)`, "gm"), (_, find) => {
4
+ return `<**${tag}**>` + (find.startsWith('</') ? 'CLS' : 'OPN')
5
+ })
6
+ .split(`<**${tag}**>`)
7
+
8
+ if (htmls.length == 1)
9
+ return htmls
10
+
11
+ let count = 0
12
+ const result: string[][] = []
13
+
14
+ for (let itm of htmls) {
15
+ let step = 0
16
+
17
+ if (itm.startsWith('OPN')) step = 1
18
+ if (itm.startsWith('CLS')) step = -1
19
+
20
+ if (step != 0)
21
+ itm = itm.substring(3)
22
+
23
+ if (step > 0) {
24
+ count+=step
25
+ step = 0
26
+ }
27
+
28
+ if (count<=1)
29
+ result.push([itm])
30
+ else
31
+ result[result.length -1].push(itm)
32
+
33
+ count+=step
34
+ }
35
+
36
+ return result.map(itms => itms.join(`<**${tag}**>`))
37
+ };
38
+
@@ -0,0 +1,67 @@
1
+ import { chromium } from "playwright"
2
+ import Task from "./ui/task"
3
+ import { spinners } from "ora"
4
+
5
+ export type input = {
6
+ method: string
7
+ url: string
8
+ input: object
9
+ }
10
+
11
+ export interface PageData {
12
+ mask: string
13
+ root: string
14
+ input: input[]
15
+ }
16
+
17
+ export const getRawPageData = async (url: string, pages: string[], debug: boolean = false): Promise<PageData[]> => {
18
+ const task = new Task()
19
+ task.next("Parsing pages", "yellow", spinners.dotsCircle)
20
+
21
+ const result: PageData[] = []
22
+
23
+ const browser = await chromium.launch({ headless: !debug, devtools: true })
24
+ const context = await browser.newContext({
25
+ userAgent: "____fast-ssr-tool___",
26
+ })
27
+
28
+ let date: any[] = []
29
+
30
+ for await (const mask of pages) {
31
+ task.next(mask, "yellow", spinners.dotsCircle, 1)
32
+
33
+ const page = await context.newPage()
34
+
35
+ const path = mask.replaceAll("{", "").replaceAll("}", "")
36
+ await page.goto(url + path)
37
+
38
+ await page.waitForLoadState("networkidle")
39
+
40
+ await page.waitForTimeout(1000)
41
+
42
+ const requests = await page.evaluate(() => {
43
+ return (window as any).__ssr_preload
44
+ })
45
+
46
+ const input = requests ? (JSON.parse(requests) as input[]) : []
47
+
48
+ const root = await page.locator("#root").innerHTML()
49
+
50
+ if (debug) {
51
+ await task.ask("continue?")
52
+ }
53
+
54
+ page.close()
55
+
56
+ result.push({
57
+ input,
58
+ mask,
59
+ root,
60
+ })
61
+ }
62
+
63
+ await browser.close()
64
+
65
+ task.success()
66
+ return result
67
+ }
@@ -0,0 +1 @@
1
+ declare module "jiti"
@@ -0,0 +1,52 @@
1
+ import ora, { Color, Ora, Spinner, spinners } from "ora";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ import readline from "node:readline/promises";
4
+
5
+ export default class Task {
6
+ spiner: Ora | undefined = undefined;
7
+
8
+ do(task: string, color?: Color, spinner?: Spinner, prefix?: string) {
9
+ this.spiner = ora({
10
+ text: task,
11
+ color,
12
+ spinner: spinner,
13
+ prefixText: prefix,
14
+ }).start();
15
+ }
16
+
17
+ next(task: string, color?: Color, spinner?: Spinner, level: number = 0) {
18
+ this.success();
19
+ this.do(
20
+ task,
21
+ color,
22
+ spinner,
23
+ level ? " ".repeat(level) + "└─" : undefined
24
+ );
25
+ }
26
+
27
+ success() {
28
+ if (this.spiner) this.spiner.succeed();
29
+ }
30
+
31
+ fail() {
32
+ if (this.spiner) this.spiner.fail();
33
+ }
34
+
35
+ async ask(question: string, defaultAnswer: string = ""): Promise<string> {
36
+ if (this.spiner) {
37
+ // this.spiner.text = `${question}: `
38
+ this.spiner.spinner = {
39
+ interval: 100,
40
+ frames: [
41
+ "?", ".", " ", "."
42
+ ]
43
+ }
44
+ }
45
+
46
+ const rl = readline.createInterface({ input, output });
47
+ const answer = (await rl.question("")).trim();
48
+ rl.close();
49
+
50
+ return answer ? answer : defaultAnswer;
51
+ }
52
+ }
@@ -0,0 +1,20 @@
1
+ import { defineConfig } from "../src/config/define";
2
+
3
+ export default defineConfig({
4
+ url: "https://fld.ru",
5
+ dev: true,
6
+ pages: [
7
+ "/",
8
+ "/products",
9
+ "/products/{klapany}",
10
+ "/product/{krany_sharovye_serii_105_prohodnoj_2h_hodovoj}",
11
+ "/{CMC-8M-4N}",
12
+ ],
13
+ // dev: true,
14
+ api: "http://nginx",
15
+ meta: "http://nginx/Api/Meta/GetTags",
16
+ export: {
17
+ outDir: "export",
18
+ index: "index.html",
19
+ },
20
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "jsx": "react-jsx",
4
+ "jsxImportSource": "react",
5
+ "module": "esnext",
6
+ "declaration": true,
7
+ "emitDeclarationOnly": true,
8
+ "outDir": "lib",
9
+ "skipLibCheck": true
10
+ },
11
+ "include": ["index.ts", "src/types"],
12
+
13
+ }