@zikojs/server 0.1.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.
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/package.json +63 -0
- package/src/code-splitter/directives.js +5 -0
- package/src/code-splitter/index.js +1 -0
- package/src/config/index.js +13 -0
- package/src/count.sh +9 -0
- package/src/entry-client/index.js +45 -0
- package/src/entry-server/index.js +39 -0
- package/src/index.js +4 -0
- package/src/prerender/index.js +63 -0
- package/src/server/api-handler.js +22 -0
- package/src/server/dev-server.js +21 -0
- package/src/server/index.js +105 -0
- package/src/server/response.js +6 -0
- package/src/server/setup-middleware.js +21 -0
- package/src/server-only-utils/glob-imports.js +47 -0
- package/src/server-only-utils/import-middlwares.js +29 -0
- package/src/server-only-utils/import-prerendered-routes.js +11 -0
- package/src/server-only-utils/index.d.ts.txt +3 -0
- package/src/server-only-utils/index.js +9 -0
- package/src/server-only-utils/manifest-parser.js +22 -0
- package/src/server-only-utils/read-files.js +20 -0
- package/src/server-only-utils/renderDomToString.js +8 -0
- package/src/server-only-utils/renderToString.js +8 -0
- package/src/server-only-utils/resolve-static-routes.js +64 -0
- package/src/server-only-utils/write-to-dist.js +24 -0
- package/src/setup/files-content.js +53 -0
- package/src/setup/generate-hydration-map.js +3 -0
- package/src/setup/generate-routes.js +22 -0
- package/src/setup/index.js +3 -0
- package/src/setup/setup-ziko-folder.js +16 -0
- package/src/template.js +14 -0
- package/src/utils/api.js +7 -0
- package/src/utils/html-template.js +19 -0
- package/src/utils/index.js +4 -0
- package/src/utils/isAsync.js +3 -0
- package/src/utils/normalize-path.js +21 -0
- package/src/utils/routes-matcher.js +216 -0
- package/src/vite/index.js +1 -0
- package/src/vite/vite_setup.js +37 -0
- package/types/global.d.ts +11 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 ZAKARIA ELALAOUI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
[](https://github.com/zikojs)
|
|
2
|
+
|
|
3
|
+
# Ziko-server
|
|
4
|
+
|
|
5
|
+
Server-side rendering for Zikojs with file-based routing and client-side hydration.
|
|
6
|
+
|
|
7
|
+
# Features
|
|
8
|
+
- ✅ Server Side Rendering
|
|
9
|
+
- ✅ Client Side Hydration with no additional config
|
|
10
|
+
- ✅ File Based Routing
|
|
11
|
+
- ✅ Mdzjs Supports
|
|
12
|
+
- ✅ Supports Dynamic Routes
|
|
13
|
+
- ✅ Supports both Sync And Async rendering
|
|
14
|
+
- ✅ Supports API
|
|
15
|
+
|
|
16
|
+
<!--
|
|
17
|
+
compare with type
|
|
18
|
+
-->
|
|
19
|
+
|
|
20
|
+
# Project Structure
|
|
21
|
+
```
|
|
22
|
+
App
|
|
23
|
+
├──public/
|
|
24
|
+
├── src/
|
|
25
|
+
├── .ziko #generated
|
|
26
|
+
├── pages/ # required
|
|
27
|
+
├── index.js
|
|
28
|
+
├── components/
|
|
29
|
+
├── layouts/
|
|
30
|
+
├── contents/
|
|
31
|
+
├── db/
|
|
32
|
+
├── i18n/
|
|
33
|
+
├── middlewares.js
|
|
34
|
+
├── tsconfig.json
|
|
35
|
+
├── ziko.config.js
|
|
36
|
+
├── package.json
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Config
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
// server.js
|
|
45
|
+
import { createServer } from "ziko-server/server";
|
|
46
|
+
createServer()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
// entry-server.js
|
|
51
|
+
import {defineServerEntry} from "ziko-server/entry-server";
|
|
52
|
+
export default defineServerEntry({
|
|
53
|
+
pages : import.meta.glob("./pages/**/*{.js,.mdz}")
|
|
54
|
+
})
|
|
55
|
+
```
|
|
56
|
+
```js
|
|
57
|
+
// entry-client.js
|
|
58
|
+
import {EntryClient} from "ziko-server/entry-client";
|
|
59
|
+
EntryClient({
|
|
60
|
+
pages : import.meta.glob("./pages/**/*{.js,.mdz}")
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
# ⭐️ Show your support
|
|
65
|
+
|
|
66
|
+
If you appreciate the project, kindly demonstrate your support by giving it a star!<br>
|
|
67
|
+
|
|
68
|
+
[](https://github.com/zakarialaoui10/ziko-server)
|
|
69
|
+
<!--## Financial support-->
|
|
70
|
+
# License
|
|
71
|
+
This projet is licensed under the terms of MIT License
|
|
72
|
+
<img src="https://img.shields.io/github/license/zakarialaoui10/ziko-server?color=rgb%2820%2C21%2C169%29" width="100" align="right">
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zikojs/server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "server side rendering in zikojs with file-based-routing and client side hydration",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"source": "src/index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"src",
|
|
9
|
+
"types",
|
|
10
|
+
"dist",
|
|
11
|
+
"LICENCE"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./src/index.js",
|
|
15
|
+
"./*": "./src/*/index.js"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"ziko",
|
|
22
|
+
"zikojs",
|
|
23
|
+
"ssr",
|
|
24
|
+
"ssg",
|
|
25
|
+
"rendering",
|
|
26
|
+
"hydration",
|
|
27
|
+
"server",
|
|
28
|
+
"middlewares"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"dev": "rollup -c rollup.config.js -w",
|
|
32
|
+
"build": "rollup -c rollup.config.js",
|
|
33
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
34
|
+
"start-demo": "cd demo && npm run dev "
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/zakarialaoui10/ziko-server.git"
|
|
39
|
+
},
|
|
40
|
+
"author": "zakaria elalaoui",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/zakarialaoui10/ziko-server/issues"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/zakarialaoui10/ziko-server#readme",
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@rollup/plugin-commonjs": "^29.0.3",
|
|
48
|
+
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
49
|
+
"@rollup/plugin-terser": "^1.0.0",
|
|
50
|
+
"rollup": "^4.62.4"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"compression": "^1.8.1",
|
|
54
|
+
"cross-env": "^10.1.0",
|
|
55
|
+
"express": "^5.2.1",
|
|
56
|
+
"extendom": "^0.0.0",
|
|
57
|
+
"fast-glob": "^3.3.3",
|
|
58
|
+
"jsdom": "^30.0.1",
|
|
59
|
+
"sirv": "^3.0.2",
|
|
60
|
+
"vite": "^8.2.1",
|
|
61
|
+
"ziko": "^1.8.0"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './directives.js'
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig as defineViteConfig } from "vite";
|
|
2
|
+
import { vite_setup } from "../vite/index.js";
|
|
3
|
+
export function defineConfig({ outDir = "dist" } = {}) {
|
|
4
|
+
return defineViteConfig(({ command, mode }) => {
|
|
5
|
+
const base_config = vite_setup({
|
|
6
|
+
outDir,
|
|
7
|
+
mode,
|
|
8
|
+
});
|
|
9
|
+
base_config.plugins = base_config.plugins || [];
|
|
10
|
+
base_config.aaa = 10
|
|
11
|
+
return base_config;
|
|
12
|
+
});
|
|
13
|
+
}
|
package/src/count.sh
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Count total lines in all .js and .ts files, excluding dist and node_modules
|
|
4
|
+
total_lines=$(find . -type f \( -name "*.js" -o -name "*.ts" \) \
|
|
5
|
+
-not -path "*/node_modules/*" \
|
|
6
|
+
-not -path "*/dist/*" \
|
|
7
|
+
-exec wc -l {} + | awk '{total += $1} END {print total}')
|
|
8
|
+
|
|
9
|
+
echo "Total JS + TS lines: $total_lines"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalize_path,
|
|
3
|
+
routesMatcher,
|
|
4
|
+
dynamicRoutesParser,
|
|
5
|
+
isDynamic,
|
|
6
|
+
} from "../utils/index.js";
|
|
7
|
+
|
|
8
|
+
export function EntryClient({ base = '', pages } = {}) {
|
|
9
|
+
|
|
10
|
+
if (import.meta.env.DEV) pages = import.meta.glob("/src/pages/**/*{.js,.mdz}");
|
|
11
|
+
pages = import.meta.glob("/src/pages/**/*{.js,.mdz}");
|
|
12
|
+
addEventListener("load", async () => {
|
|
13
|
+
const data = JSON.parse(document.head.querySelector('script#ziko-data')?.textContent ?? "{}")
|
|
14
|
+
globalThis.Ziko = data;
|
|
15
|
+
const root = "./pages/";
|
|
16
|
+
async function hydrate(path) {
|
|
17
|
+
if (path.endsWith("/")) path = path.slice(0, -1);
|
|
18
|
+
const matchEntry = Object.entries(pages).find(([route]) =>
|
|
19
|
+
routesMatcher(normalize_path(route, root), `/${path}`)
|
|
20
|
+
);
|
|
21
|
+
if (!matchEntry) return;
|
|
22
|
+
|
|
23
|
+
const [mask, moduleImport] = matchEntry;
|
|
24
|
+
|
|
25
|
+
const module = await moduleImport();
|
|
26
|
+
let UIElement;
|
|
27
|
+
|
|
28
|
+
if (isDynamic(mask)) {
|
|
29
|
+
const params = dynamicRoutesParser(mask, `/${path}`);
|
|
30
|
+
UIElement = await module.default(params);
|
|
31
|
+
} else {
|
|
32
|
+
UIElement = await module.default();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
UIElement.unmount();
|
|
36
|
+
|
|
37
|
+
const elementsToHydrate = [...document.querySelectorAll('[data-hydration-index]')];
|
|
38
|
+
elementsToHydrate.forEach(el => {
|
|
39
|
+
el.replaceWith(__Ziko__.__HYDRATION__.store.get(+el.dataset.hydrationIndex)().element);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
hydrate(location.pathname.slice(1));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
renderToString,
|
|
3
|
+
globImports,
|
|
4
|
+
readFiles,
|
|
5
|
+
} from "../server-only-utils/index.js";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
routesMatcher,
|
|
9
|
+
dynamicRoutesParser,
|
|
10
|
+
isDynamic
|
|
11
|
+
} from "../utils/routes-matcher.js";
|
|
12
|
+
|
|
13
|
+
export function EntryServer() {
|
|
14
|
+
return async function render(path, req, res) {
|
|
15
|
+
if(path.endsWith("/")) path = path.slice(0, -1);
|
|
16
|
+
// const all_files = await readFiles()
|
|
17
|
+
const pairs = await globImports("./src/pages/**/*{.js,.mdz}")
|
|
18
|
+
|
|
19
|
+
let [mask, module] = Object.entries(pairs).find(([route]) => routesMatcher(route, `/${path}`));
|
|
20
|
+
let UIElement, ui, params;
|
|
21
|
+
if (isDynamic(mask)) params = dynamicRoutesParser(mask, `/${path}`);
|
|
22
|
+
const {Component, GET, POST, DELETE, UPDATE, head, prerender} = await module
|
|
23
|
+
if(Component){
|
|
24
|
+
UIElement = params ? await Component.call(this, params) : await Component();
|
|
25
|
+
ui = renderToString(UIElement);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
head,
|
|
30
|
+
ui,
|
|
31
|
+
prerender,
|
|
32
|
+
hydration_map : __Ziko__.__HYDRATION__,
|
|
33
|
+
GET,
|
|
34
|
+
POST,
|
|
35
|
+
DELETE,
|
|
36
|
+
UPDATE,
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
globImports,
|
|
3
|
+
renderToString,
|
|
4
|
+
writeToDist,
|
|
5
|
+
ManifestParser,
|
|
6
|
+
resolveStaticRoutes,
|
|
7
|
+
} from "ziko-server/server-only-utils";
|
|
8
|
+
|
|
9
|
+
import { routesGrouper } from "../utils/routes-matcher.js";
|
|
10
|
+
|
|
11
|
+
const StaticRoutesMap = {
|
|
12
|
+
"/blog/[...slug]":[
|
|
13
|
+
{ slug : 1 },
|
|
14
|
+
{ slug : 2 },
|
|
15
|
+
{ slug : 3 }
|
|
16
|
+
],
|
|
17
|
+
"/bb/[lang]/dd/[id]":[
|
|
18
|
+
{ lang : "en", id : 1},
|
|
19
|
+
{ lang : "en", id : 2},
|
|
20
|
+
{ lang : "en", id : 3},
|
|
21
|
+
{ lang : "fr", id : 1},
|
|
22
|
+
{ lang : "fr", id : 2},
|
|
23
|
+
{ lang : "fr", id : 3}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async function prerender({outDir = 'dist'} = "") {
|
|
30
|
+
const PRERENDERED_ROUTES = []
|
|
31
|
+
const pages = await globImports("./src/pages/**/*{.js,.ts,.mdz}")
|
|
32
|
+
const StaticPages = await resolveStaticRoutes(pages, StaticRoutesMap)
|
|
33
|
+
|
|
34
|
+
const grouped = routesGrouper(pages)
|
|
35
|
+
|
|
36
|
+
const Manifest = new ManifestParser(`${outDir}/.client/.vite/manifest.json`)
|
|
37
|
+
for(let route in grouped.static){
|
|
38
|
+
const Page = grouped.static[route];
|
|
39
|
+
const {Component, head} = Page
|
|
40
|
+
if(Component){
|
|
41
|
+
const res = await Component();
|
|
42
|
+
const ui = renderToString(res)
|
|
43
|
+
PRERENDERED_ROUTES.push(route)
|
|
44
|
+
writeToDist({route, ui, head, entry_client_path : Manifest.EntryClientFile, outDir})
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for(let route in StaticPages){
|
|
48
|
+
const Page = StaticPages[route];
|
|
49
|
+
const {Component, head} = Page
|
|
50
|
+
if(Component){
|
|
51
|
+
const res = await Component();
|
|
52
|
+
const ui = renderToString(res)
|
|
53
|
+
PRERENDERED_ROUTES.push(route)
|
|
54
|
+
// console.log({hi :__Ziko__.__HYDRATION__.store, route})
|
|
55
|
+
// __Ziko__.__HYDRATION__.reset()
|
|
56
|
+
writeToDist({route, ui, head, entry_client_path : Manifest.EntryClientFile, outDir})
|
|
57
|
+
}
|
|
58
|
+
// console.log(PRERENDERED_ROUTES)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
export { prerender }
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export async function API_HANDLER(fn, req, res) {
|
|
2
|
+
const result = await fn(req, res);
|
|
3
|
+
|
|
4
|
+
if (result instanceof Response) {
|
|
5
|
+
const body = await result.text();
|
|
6
|
+
res
|
|
7
|
+
.status(result.status || 200)
|
|
8
|
+
.set(Object.fromEntries(result.headers.entries()))
|
|
9
|
+
.send(body);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (typeof result === "object" && result !== null) {
|
|
13
|
+
res.json(result);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (["string", "number", "boolean"].includes(typeof result)) {
|
|
17
|
+
res.send(String(result));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (result === undefined) return;
|
|
21
|
+
res.status(500).send("Unsupported response type");
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const dev_server = async (app, base)=>{
|
|
2
|
+
const { createServer } = await import("vite");
|
|
3
|
+
let vite = await createServer({
|
|
4
|
+
server: { middlewareMode: true },
|
|
5
|
+
appType: "custom",
|
|
6
|
+
base,
|
|
7
|
+
});
|
|
8
|
+
app.use(vite.middlewares);
|
|
9
|
+
return vite
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// import {join} from 'path'
|
|
13
|
+
// export const prod_server = async (app, base, baseDir)=>{
|
|
14
|
+
// const compression = (await import("compression")).default;
|
|
15
|
+
// const sirv = (await import("sirv")).default;
|
|
16
|
+
// app.use(compression());
|
|
17
|
+
// app.use(
|
|
18
|
+
// base,
|
|
19
|
+
// sirv(join(baseDir, "./dist/.client"), { extensions: [] }),
|
|
20
|
+
// );
|
|
21
|
+
// }
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path, { join } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { dev_server } from "./dev-server.js";
|
|
6
|
+
import { API_HANDLER } from "./api-handler.js";
|
|
7
|
+
import {
|
|
8
|
+
importMiddlewares,
|
|
9
|
+
importPrerenderedRoutes,
|
|
10
|
+
} from '../server-only-utils/index.js'
|
|
11
|
+
|
|
12
|
+
import { setup_ziko_folder } from "../setup/setup-ziko-folder.js";
|
|
13
|
+
import { SetupMiddleware } from './setup-middleware.js'
|
|
14
|
+
|
|
15
|
+
export async function createServer({ baseDir = process.cwd(), port = process.env.PORT || 5173 } = {}){
|
|
16
|
+
await setup_ziko_folder(join(baseDir, './.ziko-experimental'))
|
|
17
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
18
|
+
const base = process.env.BASE || "/";
|
|
19
|
+
const HTML_TEMPLATE = isProduction ? await readFile(join(baseDir, "./dist/.client/.ziko/index.html"), "utf-8") : "";
|
|
20
|
+
const app = express();
|
|
21
|
+
|
|
22
|
+
const Middlewares = await importMiddlewares()
|
|
23
|
+
app.use(Middlewares.logger)
|
|
24
|
+
app.use(SetupMiddleware)
|
|
25
|
+
|
|
26
|
+
let vite;
|
|
27
|
+
if (!isProduction) vite = await dev_server(app, base)
|
|
28
|
+
else {
|
|
29
|
+
const compression = (await import("compression")).default;
|
|
30
|
+
const sirv = (await import("sirv")).default;
|
|
31
|
+
app.use(compression());
|
|
32
|
+
app.use(
|
|
33
|
+
base,
|
|
34
|
+
sirv(join(baseDir, "./dist/.client"), { extensions: [] }),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Serve HTML
|
|
39
|
+
app.use('/.client', express.static(path.join(process.cwd(), 'dist/.client')))
|
|
40
|
+
// app.use(express.static('public'))
|
|
41
|
+
|
|
42
|
+
if(isProduction){
|
|
43
|
+
const PRERENDERED_ROUTES = await importPrerenderedRoutes()
|
|
44
|
+
for(let i=0; i<PRERENDERED_ROUTES.length; i++){
|
|
45
|
+
app.get(PRERENDERED_ROUTES[i], (req, res)=>{
|
|
46
|
+
res.sendFile(path.join(process.cwd(), `dist/${PRERENDERED_ROUTES[i]}/index.html`))
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
app.get('/--ziko--', (req, res)=>{
|
|
53
|
+
res.json(globalThis.Ziko)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
app.use(async (req, res) => {
|
|
57
|
+
try {
|
|
58
|
+
const url = req.originalUrl.replace(base, "");
|
|
59
|
+
let template;
|
|
60
|
+
let render;
|
|
61
|
+
if (!isProduction) {
|
|
62
|
+
template = await readFile(join(baseDir, "./.ziko/index.html"), "utf-8");
|
|
63
|
+
template = await vite.transformIndexHtml(url, template);
|
|
64
|
+
render = (await vite.ssrLoadModule("/.ziko/entry-server.js")).default;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
template = HTML_TEMPLATE;
|
|
68
|
+
const ENTRY_SERVER_PATH = pathToFileURL(join(baseDir, "./dist/.server/entry-server.js")).href;
|
|
69
|
+
render = (await import(/* @vite-ignore */ENTRY_SERVER_PATH)).default;
|
|
70
|
+
}
|
|
71
|
+
const rendered = await render(url, req);
|
|
72
|
+
const page = await rendered(url, req);
|
|
73
|
+
if(page.GET) return await API_HANDLER(page.GET, req, res)
|
|
74
|
+
if(page.POST) return await API_HANDLER(page.POST, req, res)
|
|
75
|
+
if(page.PUT) return await API_HANDLER(page.PUT, req, res)
|
|
76
|
+
if(page.DELETE) return await API_HANDLER(page.DELETE, req, res)
|
|
77
|
+
if(page.PATCH) return await API_HANDLER(page.PATCH, req, res)
|
|
78
|
+
|
|
79
|
+
const html = template
|
|
80
|
+
.replace(
|
|
81
|
+
'<!--app-session-data-->',
|
|
82
|
+
`<script type='application/json' id='ziko-data'>
|
|
83
|
+
${JSON.stringify(Ziko, null, 2)}
|
|
84
|
+
</script>
|
|
85
|
+
`
|
|
86
|
+
)
|
|
87
|
+
// .replace(`<!--app-head-->`, page.head ?? "")
|
|
88
|
+
.replace(`<!--app-html-->`, page.ui ?? "")
|
|
89
|
+
|
|
90
|
+
res.status(200).set({ "Content-Type": "text/html" }).send(html);
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
vite?.ssrFixStacktrace(e);
|
|
94
|
+
res.status(500).end(e.stack);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
app.listen(port, () => {
|
|
99
|
+
console.log(`Server started at http://localhost:${port}`);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return app;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// import { join } from 'path';
|
|
2
|
+
// import { pathToFileURL } from 'url';
|
|
3
|
+
// const ziko_config_path = pathToFileURL(join(process.cwd(), 'ziko.config.js')).href;
|
|
4
|
+
|
|
5
|
+
export function SetupMiddleware(req, res, next) {
|
|
6
|
+
const protocol = req.protocol || (req.connection.encrypted ? 'https' : 'http');
|
|
7
|
+
const host = req.headers.host;
|
|
8
|
+
const origin = `${protocol}://${host}`
|
|
9
|
+
globalThis.Ziko = {
|
|
10
|
+
engine: 'zikojs',
|
|
11
|
+
isProd : process.env.NODE_ENV === "production",
|
|
12
|
+
url : req.url,
|
|
13
|
+
protocol,
|
|
14
|
+
host,
|
|
15
|
+
origin,
|
|
16
|
+
locals: {},
|
|
17
|
+
};
|
|
18
|
+
if(globalThis?.__Ziko__) __Ziko__.__HYDRATION__.reset();
|
|
19
|
+
Object.assign(req, {Ziko})
|
|
20
|
+
next();
|
|
21
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fg from 'fast-glob';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
import { normalize_path } from '../utils/normalize-path.js';
|
|
5
|
+
|
|
6
|
+
export async function globImports(pattern = './src/pages/**/*.{js,ts,jsx,tsx,mdz}', { cwd = process.cwd() , root = "./pages/"} = {}) {
|
|
7
|
+
const files = await fg(pattern, { cwd });
|
|
8
|
+
const modules = {};
|
|
9
|
+
|
|
10
|
+
for (const file of files) {
|
|
11
|
+
const absPath = path.resolve(cwd, file);
|
|
12
|
+
const fileUrl = pathToFileURL(absPath).href;
|
|
13
|
+
const key = './' + file.replace(/\\/g, '/');
|
|
14
|
+
modules[key] = () => import(/* @vite-ignore */fileUrl);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const routes = Object.keys(modules);
|
|
18
|
+
|
|
19
|
+
const pairs = {};
|
|
20
|
+
for (let i = 0; i < routes.length; i++) {
|
|
21
|
+
const module = await modules[routes[i]]();
|
|
22
|
+
const {
|
|
23
|
+
default : Component,
|
|
24
|
+
head,
|
|
25
|
+
prerender,
|
|
26
|
+
GET,
|
|
27
|
+
POST,
|
|
28
|
+
PUT,
|
|
29
|
+
DELETE,
|
|
30
|
+
PATCH
|
|
31
|
+
} = await module
|
|
32
|
+
|
|
33
|
+
Object.assign(pairs, {
|
|
34
|
+
[normalize_path(routes[i], root)]: {
|
|
35
|
+
...(Component && {Component}),
|
|
36
|
+
...(head && {head}),
|
|
37
|
+
...(prerender !== undefined && {prerender}),
|
|
38
|
+
...(GET && {GET}),
|
|
39
|
+
...(POST && {POST}),
|
|
40
|
+
...(PUT && {PUT}),
|
|
41
|
+
...(PATCH && {PATCH}),
|
|
42
|
+
...(DELETE && {DELETE}),
|
|
43
|
+
} });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return pairs
|
|
47
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
|
|
5
|
+
export async function importMiddlewares({ cwd = process.cwd() } = {}) {
|
|
6
|
+
const possibleFiles = [
|
|
7
|
+
path.join(cwd, 'src', 'middleware.js'),
|
|
8
|
+
path.join(cwd, 'src', 'middleware.ts')
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
for (const file of possibleFiles) {
|
|
12
|
+
if (fs.existsSync(file)) {
|
|
13
|
+
const fileUrl = pathToFileURL(file).href;
|
|
14
|
+
const module = await import(/* @vite-ignore */ fileUrl);
|
|
15
|
+
|
|
16
|
+
const middlewares = {};
|
|
17
|
+
for (const [key, value] of Object.entries(module)) {
|
|
18
|
+
if (typeof value === 'function') {
|
|
19
|
+
middlewares[key] = value;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return middlewares;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// No middleware file found
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { pathToFileURL } from 'url';
|
|
3
|
+
|
|
4
|
+
export async function importPrerenderedRoutes({ cwd = process.cwd(), isProduction } = {}) {
|
|
5
|
+
const file = isProduction
|
|
6
|
+
? join(cwd, 'dist/.server/prerenderd-routes.js')
|
|
7
|
+
: join(cwd, '.ziko/cache/.prerenderd-routes.js')
|
|
8
|
+
const FileUrl = pathToFileURL(file).href;
|
|
9
|
+
const module = await import(/* @vite-ignore */ FileUrl);
|
|
10
|
+
return module?.PRERENDERED_ROUTES || []
|
|
11
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./renderToString.js";
|
|
2
|
+
export * from "./renderDomToString.js";
|
|
3
|
+
export * from "./glob-imports.js";
|
|
4
|
+
export * from "./write-to-dist.js";
|
|
5
|
+
export * from './manifest-parser.js';
|
|
6
|
+
export * from './import-prerendered-routes.js'
|
|
7
|
+
export * from './import-middlwares.js'
|
|
8
|
+
export * from './resolve-static-routes.js'
|
|
9
|
+
export * from './read-files.js'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {readFileSync, existsSync} from 'fs';
|
|
2
|
+
import {join} from 'path';
|
|
3
|
+
|
|
4
|
+
export class ManifestParser {
|
|
5
|
+
constructor(relative_path){
|
|
6
|
+
this.#init(relative_path)
|
|
7
|
+
}
|
|
8
|
+
#init(relative_path){
|
|
9
|
+
const file = join(process.cwd(), relative_path);
|
|
10
|
+
if (existsSync(file)) this.manifest = JSON.parse(readFileSync(file, "utf-8"));
|
|
11
|
+
else {
|
|
12
|
+
console.error(`Manifest file not found: ${file}`)
|
|
13
|
+
this.manifest = null
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
get EntryClientFile(){
|
|
17
|
+
return this.manifest['.ziko/entry-client.js'].file
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// const manifest = new ManifestParser('./dist/client/.vite/manifest.json')
|
|
22
|
+
// console.log(manifest.files)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fg from "fast-glob";
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import { resolve, relative } from "path";
|
|
4
|
+
|
|
5
|
+
export async function readFiles(pattern = './src/**/*.{js,ts}', { cwd = process.cwd() , root = "./pages/"} = {}) {
|
|
6
|
+
const files = await fg(pattern, { cwd });
|
|
7
|
+
const absoluteRoot = resolve(root);
|
|
8
|
+
console.log({files})
|
|
9
|
+
const modules = {};
|
|
10
|
+
|
|
11
|
+
const result = new Map()
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
const code = await readFile(file, "utf8");
|
|
14
|
+
if(!code.includes('use client')) continue;
|
|
15
|
+
// Normalize route to something like "src/pages/index.js"
|
|
16
|
+
const route = relative(absoluteRoot, file).replace(/\\/g, "/");
|
|
17
|
+
result.set(route, code);
|
|
18
|
+
}
|
|
19
|
+
console.log(result)
|
|
20
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export async function resolveStaticRoutes(routes, StaticRoutesMap) {
|
|
2
|
+
const result = {};
|
|
3
|
+
|
|
4
|
+
for (const [routePattern, handler] of Object.entries(routes)) {
|
|
5
|
+
const { Component, prerender, head } = handler;
|
|
6
|
+
if(!Component || prerender === false) continue;
|
|
7
|
+
const staticParamsList = StaticRoutesMap[routePattern];
|
|
8
|
+
if (staticParamsList && /\[.*\]/.test(routePattern)) {
|
|
9
|
+
for (const params of staticParamsList) {
|
|
10
|
+
let resolvedRoute = routePattern;
|
|
11
|
+
|
|
12
|
+
// Handle [...param] → can include slashes
|
|
13
|
+
resolvedRoute = resolvedRoute.replace(/\[\.\.\.(\w+)\]/g, (_, key) => {
|
|
14
|
+
return encodeURIComponent(params[key])?.replace(/%2F/g, '/');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Handle [param]+ or [param]
|
|
18
|
+
resolvedRoute = resolvedRoute.replace(/\[(\w+)\]\+?/g, (_, key) => {
|
|
19
|
+
return encodeURIComponent(params[key]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
result[resolvedRoute] = {
|
|
23
|
+
Component : () => Component(params),
|
|
24
|
+
head
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
result[routePattern] = {
|
|
29
|
+
Component,
|
|
30
|
+
head
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
// DEMO
|
|
40
|
+
|
|
41
|
+
// const StaticRoutesMap = {
|
|
42
|
+
// "/blog/[...slug]": [
|
|
43
|
+
// { slug: "2025/oct/post1" },
|
|
44
|
+
// { slug: "2025/oct/post2" },
|
|
45
|
+
// { slug: "2025/nov/post3" }
|
|
46
|
+
// ],
|
|
47
|
+
// "/user/[id]+": [
|
|
48
|
+
// { id: 1 },
|
|
49
|
+
// { id: 2 },
|
|
50
|
+
// { id: 3 }
|
|
51
|
+
// ],
|
|
52
|
+
// "/product/[category]/[id]": [
|
|
53
|
+
// { category: "books", id: 12 },
|
|
54
|
+
// { category: "tech", id: 5 }
|
|
55
|
+
// ]
|
|
56
|
+
// };
|
|
57
|
+
|
|
58
|
+
// const routes = {
|
|
59
|
+
// "/blog/[...slug]": (params) => `Blog page for ${params.slug}`,
|
|
60
|
+
// "/user/[id]+": (params) => `User ${params.id}`,
|
|
61
|
+
// "/product/[category]/[id]": (params) => `Product ${params.category} #${params.id}`
|
|
62
|
+
// };
|
|
63
|
+
|
|
64
|
+
// resolveStaticRoutes(routes, StaticRoutesMap).then(console.log);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {mkdir, writeFile} from 'fs/promises';
|
|
2
|
+
import {join, dirname} from 'path';
|
|
3
|
+
|
|
4
|
+
export async function writeToDist({route, ui, head, outDir = 'dist', entry_client_path = ''}={}) {
|
|
5
|
+
entry_client_path = `/.client/${entry_client_path}`
|
|
6
|
+
const out = `
|
|
7
|
+
<!doctype html>
|
|
8
|
+
<html>
|
|
9
|
+
<head>
|
|
10
|
+
<script type="module" src="${entry_client_path}"></script>
|
|
11
|
+
<script type='application/json' id='ziko-data'>
|
|
12
|
+
${JSON.stringify(globalThis?.Ziko ?? {}, null, 2)}
|
|
13
|
+
</script>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
${ui}
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
|
19
|
+
`
|
|
20
|
+
const filePath = join(outDir, route === '/' ? '' : route, 'index.html');
|
|
21
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
22
|
+
await writeFile(filePath, out, 'utf8');
|
|
23
|
+
console.log(`✔️ Saved ${route} → ${filePath}`);
|
|
24
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const HTMLINDEX = `
|
|
2
|
+
<!doctype html>
|
|
3
|
+
<html>
|
|
4
|
+
<head>
|
|
5
|
+
<title>Ziko - App </title>
|
|
6
|
+
<meta charset="UTF-8" />
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
9
|
+
<script type="module" src="/.ziko/entry-client.js"></script>
|
|
10
|
+
<!--app-head-->
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<!--app-html-->
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
|
16
|
+
`.trim()
|
|
17
|
+
|
|
18
|
+
const SERVER = `
|
|
19
|
+
import { createServer } from "ziko-server/server";
|
|
20
|
+
createServer()
|
|
21
|
+
`.trim()
|
|
22
|
+
|
|
23
|
+
const ENTRY_SERVER = `
|
|
24
|
+
import {EntryServer} from "ziko-server/entry-server";
|
|
25
|
+
export default EntryServer
|
|
26
|
+
`.trim()
|
|
27
|
+
|
|
28
|
+
const ENTRY_CLIENT = `
|
|
29
|
+
import {EntryClient} from "ziko-server/entry-client";
|
|
30
|
+
EntryClient({
|
|
31
|
+
base : new URL(".", import.meta.url).pathname
|
|
32
|
+
})
|
|
33
|
+
`.trim()
|
|
34
|
+
|
|
35
|
+
const PRERENDER_SCRIPT = `
|
|
36
|
+
import {prerender} from 'ziko-server'
|
|
37
|
+
prerender()
|
|
38
|
+
`
|
|
39
|
+
|
|
40
|
+
const GENERATE_ROUTES_SCRIPT = `
|
|
41
|
+
import { generate_routes } from "ziko-server/setup";
|
|
42
|
+
generate_routes()
|
|
43
|
+
`
|
|
44
|
+
|
|
45
|
+
export const Files_Content = {
|
|
46
|
+
'index.html': HTMLINDEX,
|
|
47
|
+
'server.js' : SERVER,
|
|
48
|
+
'entry-server.js' : ENTRY_SERVER,
|
|
49
|
+
'entry-client.js' : ENTRY_CLIENT,
|
|
50
|
+
'scripts/prerender.js' : PRERENDER_SCRIPT,
|
|
51
|
+
'scripts/generate-routes.js' : GENERATE_ROUTES_SCRIPT,
|
|
52
|
+
'cache/.keep' : null
|
|
53
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {writeFileSync} from "fs"
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
const __pages__ = {
|
|
5
|
+
"/src/pages/index.js": () => import("/src/pages/index.js"),
|
|
6
|
+
"/src/pages/me.js": () => import("/src/pages/me.js"),
|
|
7
|
+
"/src/pages/about/index.js": () => import("/src/pages/about/index.js"),
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function generate_routes(){
|
|
11
|
+
const Pages = stringify(__pages__);
|
|
12
|
+
const Output = `export const pages = ${Pages}`
|
|
13
|
+
const path = join(process.cwd(), './.ziko/cache/.generated-routes.js')
|
|
14
|
+
writeFileSync(path, Output)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function stringify(obj) {
|
|
18
|
+
const entries = Object.entries(obj).map(
|
|
19
|
+
([key, val]) => ` "${key}": ${val.toString()}`
|
|
20
|
+
);
|
|
21
|
+
return `{\n${entries.join(",\n")}\n}`;
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'fs/promises';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { Files_Content } from './files-content.js';
|
|
4
|
+
|
|
5
|
+
export async function setup_ziko_folder(basePath) {
|
|
6
|
+
for (const [file, content] of Object.entries(Files_Content)) {
|
|
7
|
+
const fullPath = join(basePath, file);
|
|
8
|
+
const dir = dirname(fullPath);
|
|
9
|
+
await mkdir(dir, { recursive: true });
|
|
10
|
+
try {
|
|
11
|
+
const existing = await readFile(fullPath, 'utf8');
|
|
12
|
+
if (existing === content) continue;
|
|
13
|
+
} catch {}
|
|
14
|
+
await writeFile(fullPath, content ?? '', 'utf8');
|
|
15
|
+
}
|
|
16
|
+
}
|
package/src/template.js
ADDED
package/src/utils/api.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const HTMLTemplate = `
|
|
2
|
+
<!doctype html>
|
|
3
|
+
<html lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<title>Z</title>
|
|
6
|
+
<meta charset="UTF-8" />
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
9
|
+
<meta name="description" content="test ziko-ssr"/>
|
|
10
|
+
<title>Vite App</title>
|
|
11
|
+
<!--app-head-->
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<!--app-html-->
|
|
15
|
+
<script type="module" src="/src/entry-client.js" async></script>
|
|
16
|
+
<!-- <script type="module" src="node_modules/ziko-server/entry/client.js" async></script> -->
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
|
19
|
+
`
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function normalize_path(inputPath, root = './src/pages', extensions = ['js', 'ts']) {
|
|
2
|
+
if(root.at(-1)==="/") root = root.slice(0, -1)
|
|
3
|
+
const normalizedPath = inputPath.replace(/\\/g, '/')
|
|
4
|
+
// .replace(/\[(\w+)\]/g, '$1/:$1');
|
|
5
|
+
const parts = normalizedPath.split('/');
|
|
6
|
+
const rootParts = root.split('/');
|
|
7
|
+
const rootIndex = parts.indexOf(rootParts[rootParts.length - 1]);
|
|
8
|
+
if (rootIndex !== -1) {
|
|
9
|
+
const subsequentParts = parts.slice(rootIndex + 1);
|
|
10
|
+
const lastPart = subsequentParts[subsequentParts.length - 1];
|
|
11
|
+
const isIndexFile = lastPart === 'index.js' || lastPart === 'index.ts';
|
|
12
|
+
const hasValidExtension = extensions.some(ext => lastPart === `.${ext}` || lastPart.endsWith(`.${ext}`));
|
|
13
|
+
if (isIndexFile) {
|
|
14
|
+
return '/' + (subsequentParts.length > 1 ? subsequentParts.slice(0, -1).join('/') : '');
|
|
15
|
+
}
|
|
16
|
+
if (hasValidExtension) {
|
|
17
|
+
return '/' + subsequentParts.join('/').replace(/\.(js|ts)$/, '');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
function routesMatcher(mask, route) {
|
|
2
|
+
const maskSegments = mask.split("/").filter(Boolean);
|
|
3
|
+
const routeSegments = route.split("/").filter(Boolean);
|
|
4
|
+
|
|
5
|
+
let i = 0,
|
|
6
|
+
j = 0;
|
|
7
|
+
|
|
8
|
+
while (i < maskSegments.length && j < routeSegments.length) {
|
|
9
|
+
const maskSegment = maskSegments[i];
|
|
10
|
+
const routeSegment = routeSegments[j];
|
|
11
|
+
|
|
12
|
+
// Catch-all segment `[...param]`
|
|
13
|
+
if (maskSegment.startsWith("[...") && maskSegment.endsWith("]")) {
|
|
14
|
+
const remainingMaskSegments = maskSegments.length - i - 1;
|
|
15
|
+
|
|
16
|
+
if (remainingMaskSegments === 0) {
|
|
17
|
+
// Last segment, matches all remaining
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Calculate minimum required route segments for remaining mask
|
|
22
|
+
let requiredSegments = 0;
|
|
23
|
+
for (let k = i + 1; k < maskSegments.length; k++) {
|
|
24
|
+
if (!maskSegments[k].endsWith("]+")) {
|
|
25
|
+
requiredSegments++;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const remainingRouteSegments = routeSegments.length - j;
|
|
30
|
+
if (remainingRouteSegments < requiredSegments) return false;
|
|
31
|
+
|
|
32
|
+
const segmentsToConsume = remainingRouteSegments - requiredSegments;
|
|
33
|
+
if (segmentsToConsume < 1) return false;
|
|
34
|
+
|
|
35
|
+
j += segmentsToConsume;
|
|
36
|
+
i++;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Optional `[param]+`
|
|
41
|
+
if (maskSegment.startsWith("[") && maskSegment.endsWith("]+")) {
|
|
42
|
+
if (routeSegment) j++;
|
|
43
|
+
i++;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Dynamic `[param]`
|
|
48
|
+
if (maskSegment.startsWith("[") && maskSegment.endsWith("]")) {
|
|
49
|
+
i++;
|
|
50
|
+
j++;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Must match static segment
|
|
55
|
+
if (maskSegment !== routeSegment) return false;
|
|
56
|
+
|
|
57
|
+
i++;
|
|
58
|
+
j++;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Handle optional parameters at the end
|
|
62
|
+
while (i < maskSegments.length) {
|
|
63
|
+
const seg = maskSegments[i];
|
|
64
|
+
if (seg.endsWith("]+")) {
|
|
65
|
+
i++;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return i === maskSegments.length && j === routeSegments.length;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function dynamicRoutesParser(mask, route) {
|
|
75
|
+
const maskSegments = mask.split("/").filter(Boolean);
|
|
76
|
+
const routeSegments = route.split("/").filter(Boolean);
|
|
77
|
+
const params = {};
|
|
78
|
+
|
|
79
|
+
let i = 0,
|
|
80
|
+
j = 0;
|
|
81
|
+
|
|
82
|
+
while (i < maskSegments.length && j < routeSegments.length) {
|
|
83
|
+
const maskSegment = maskSegments[i];
|
|
84
|
+
const routeSegment = routeSegments[j];
|
|
85
|
+
|
|
86
|
+
// Catch-all `[...param]`
|
|
87
|
+
if (maskSegment.startsWith("[...") && maskSegment.endsWith("]")) {
|
|
88
|
+
const paramName = maskSegment.slice(4, -1);
|
|
89
|
+
|
|
90
|
+
const remainingMaskSegments = maskSegments.length - i - 1;
|
|
91
|
+
if (remainingMaskSegments === 0) {
|
|
92
|
+
params[paramName] = routeSegments.slice(j).join("/");
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let requiredSegments = 0;
|
|
97
|
+
for (let k = i + 1; k < maskSegments.length; k++) {
|
|
98
|
+
if (!maskSegments[k].endsWith("]+")) {
|
|
99
|
+
requiredSegments++;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const remainingRouteSegments = routeSegments.length - j;
|
|
104
|
+
const segmentsToConsume = remainingRouteSegments - requiredSegments;
|
|
105
|
+
|
|
106
|
+
if (segmentsToConsume >= 1) {
|
|
107
|
+
params[paramName] = routeSegments
|
|
108
|
+
.slice(j, j + segmentsToConsume)
|
|
109
|
+
.join("/");
|
|
110
|
+
j += segmentsToConsume;
|
|
111
|
+
} else {
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
i++;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Optional `[param]+`
|
|
120
|
+
if (maskSegment.startsWith("[") && maskSegment.endsWith("]+")) {
|
|
121
|
+
const paramName = maskSegment.slice(1, -2);
|
|
122
|
+
if (routeSegment) {
|
|
123
|
+
params[paramName] = routeSegment;
|
|
124
|
+
j++;
|
|
125
|
+
}
|
|
126
|
+
i++;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Dynamic `[param]`
|
|
131
|
+
if (maskSegment.startsWith("[") && maskSegment.endsWith("]")) {
|
|
132
|
+
const paramName = maskSegment.slice(1, -1);
|
|
133
|
+
params[paramName] = routeSegment;
|
|
134
|
+
} else if (maskSegment !== routeSegment) {
|
|
135
|
+
return {};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
i++;
|
|
139
|
+
j++;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return params;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isDynamic(path) {
|
|
146
|
+
const DynamicPattern = /(:\w+|\[\.\.\.\w+\]|\[\w+\]\+?)/;
|
|
147
|
+
return DynamicPattern.test(path);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
function routesGrouper(routeMap) {
|
|
152
|
+
const grouped = {
|
|
153
|
+
static: {},
|
|
154
|
+
dynamic: {},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
for (const [path, value] of Object.entries(routeMap)) {
|
|
158
|
+
if (isDynamic(path)) {
|
|
159
|
+
const segments = path.split("/").filter(Boolean);
|
|
160
|
+
const optionalIndex = segments.findIndex(seg => seg.endsWith("]+"));
|
|
161
|
+
const hasInvalidOptional =
|
|
162
|
+
optionalIndex !== -1 && optionalIndex !== segments.length - 1;
|
|
163
|
+
|
|
164
|
+
if (hasInvalidOptional) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Invalid optional param position in route: "${path}" — optional parameters can only appear at the end.`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
grouped.dynamic[path] = value;
|
|
171
|
+
} else {
|
|
172
|
+
grouped.static[path] = value;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return grouped;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
export { routesMatcher, dynamicRoutesParser, isDynamic, routesGrouper };
|
|
181
|
+
|
|
182
|
+
// // DEMO
|
|
183
|
+
// console.log("=== EXISTING TESTS ===");
|
|
184
|
+
// console.log(routesMatcher("/user/[id]+", "/user")); // true
|
|
185
|
+
// console.log(routesMatcher("/user/[id]+", "/user/42")); // true
|
|
186
|
+
// console.log(routesMatcher("/blog/[...slug]", "/blog/a/b")); // true
|
|
187
|
+
// console.log(routesMatcher("/blog/[id]", "/blog")); // false
|
|
188
|
+
// console.log(routesMatcher("/product/:id", "/product/99")); // true
|
|
189
|
+
|
|
190
|
+
// console.log("\n=== PARSER TESTS ===");
|
|
191
|
+
// console.log(dynamicRoutesParser("/user/[id]+", "/user"));
|
|
192
|
+
// // 👉 {}
|
|
193
|
+
|
|
194
|
+
// console.log(dynamicRoutesParser("/user/[id]+", "/user/42"));
|
|
195
|
+
// // 👉 { id: "42" }
|
|
196
|
+
|
|
197
|
+
// console.log(dynamicRoutesParser("/blog/[...slug]", "/blog/2025/oct/post"));
|
|
198
|
+
// // 👉 { slug: "2025/oct/post" }
|
|
199
|
+
|
|
200
|
+
// console.log(
|
|
201
|
+
// dynamicRoutesParser("/product/[category]/[id]+", "/product/electronics"),
|
|
202
|
+
// );
|
|
203
|
+
// // 👉 { category: "electronics" }
|
|
204
|
+
|
|
205
|
+
// console.log("\n=== FIX TEST ===");
|
|
206
|
+
// console.log(dynamicRoutesParser("/[...slug]/[id]", "/sl1/sl2/9"));
|
|
207
|
+
// // 👉 { slug: "sl1/sl2", id: "9" }
|
|
208
|
+
|
|
209
|
+
// console.log(dynamicRoutesParser("/[slug]/[...id]", "/sl1/id1/id2"));
|
|
210
|
+
// // 👉 { slug: "sl1", id: "id1/id2" }
|
|
211
|
+
|
|
212
|
+
// console.log(dynamicRoutesParser("/blog/lang/[lang]/id/[id]", "/blog/lang/en/id/10"));
|
|
213
|
+
// // 👉 { lang: "en", id: "10" }
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
// // Only the last one that can be optional
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './vite_setup.js'
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { resolve } from 'path'
|
|
2
|
+
export function vite_setup({outDir = 'dist'} = {}){
|
|
3
|
+
const Target = process.env.TARGET;
|
|
4
|
+
const isClient = (Target === 'client')
|
|
5
|
+
const isServer = (Target === 'server')
|
|
6
|
+
if(isClient){
|
|
7
|
+
return {
|
|
8
|
+
build : {
|
|
9
|
+
modulePreload: false,
|
|
10
|
+
outDir : `${outDir}/.client`,
|
|
11
|
+
emptyOutDir: true,
|
|
12
|
+
manifest: true,
|
|
13
|
+
rollupOptions: {
|
|
14
|
+
input: {
|
|
15
|
+
main : resolve(process.cwd(), ".ziko/entry-client.js"),
|
|
16
|
+
index : resolve(process.cwd(), '.ziko/index.html')
|
|
17
|
+
},
|
|
18
|
+
output: {
|
|
19
|
+
entryFileNames: `assets/[name].[hash].js`,
|
|
20
|
+
chunkFileNames: `assets/[name].[hash].js`,
|
|
21
|
+
assetFileNames: `assets/[name].[hash].[ext]`,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if(isServer){
|
|
28
|
+
return {
|
|
29
|
+
build : {
|
|
30
|
+
outDir : `${outDir}/.server`,
|
|
31
|
+
ssr : resolve(process.cwd(), ".ziko/entry-server.js"),
|
|
32
|
+
copyPublicDir: false,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return {}
|
|
37
|
+
}
|