@teyik0/furin 0.1.0-alpha.3

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.
Files changed (71) hide show
  1. package/dist/adapter/bun.d.ts +3 -0
  2. package/dist/build/client.d.ts +14 -0
  3. package/dist/build/compile-entry.d.ts +22 -0
  4. package/dist/build/entry-template.d.ts +13 -0
  5. package/dist/build/hydrate.d.ts +20 -0
  6. package/dist/build/index.d.ts +7 -0
  7. package/dist/build/index.js +2212 -0
  8. package/dist/build/route-types.d.ts +20 -0
  9. package/dist/build/scan-server.d.ts +8 -0
  10. package/dist/build/server-routes-entry.d.ts +22 -0
  11. package/dist/build/shared.d.ts +12 -0
  12. package/dist/build/types.d.ts +53 -0
  13. package/dist/cli/config.d.ts +9 -0
  14. package/dist/cli/index.d.ts +1 -0
  15. package/dist/cli/index.js +2240 -0
  16. package/dist/client.d.ts +158 -0
  17. package/dist/client.js +20 -0
  18. package/dist/config.d.ts +16 -0
  19. package/dist/config.js +23 -0
  20. package/dist/furin.d.ts +45 -0
  21. package/dist/furin.js +937 -0
  22. package/dist/internal.d.ts +18 -0
  23. package/dist/link.d.ts +119 -0
  24. package/dist/link.js +281 -0
  25. package/dist/plugin/index.d.ts +20 -0
  26. package/dist/plugin/index.js +1408 -0
  27. package/dist/plugin/transform-client.d.ts +9 -0
  28. package/dist/render/assemble.d.ts +13 -0
  29. package/dist/render/cache.d.ts +7 -0
  30. package/dist/render/element.d.ts +4 -0
  31. package/dist/render/index.d.ts +26 -0
  32. package/dist/render/loaders.d.ts +12 -0
  33. package/dist/render/shell.d.ts +17 -0
  34. package/dist/render/template.d.ts +4 -0
  35. package/dist/router.d.ts +32 -0
  36. package/dist/router.js +575 -0
  37. package/dist/runtime-env.d.ts +3 -0
  38. package/dist/tsconfig.dts.tsbuildinfo +1 -0
  39. package/dist/utils.d.ts +6 -0
  40. package/package.json +74 -0
  41. package/src/adapter/README.md +13 -0
  42. package/src/adapter/bun.ts +119 -0
  43. package/src/build/client.ts +110 -0
  44. package/src/build/compile-entry.ts +99 -0
  45. package/src/build/entry-template.ts +62 -0
  46. package/src/build/hydrate.ts +106 -0
  47. package/src/build/index.ts +120 -0
  48. package/src/build/route-types.ts +88 -0
  49. package/src/build/scan-server.ts +88 -0
  50. package/src/build/server-routes-entry.ts +38 -0
  51. package/src/build/shared.ts +80 -0
  52. package/src/build/types.ts +60 -0
  53. package/src/cli/config.ts +68 -0
  54. package/src/cli/index.ts +106 -0
  55. package/src/client.ts +237 -0
  56. package/src/config.ts +31 -0
  57. package/src/furin.ts +251 -0
  58. package/src/internal.ts +36 -0
  59. package/src/link.tsx +480 -0
  60. package/src/plugin/index.ts +80 -0
  61. package/src/plugin/transform-client.ts +372 -0
  62. package/src/render/assemble.ts +57 -0
  63. package/src/render/cache.ts +9 -0
  64. package/src/render/element.tsx +28 -0
  65. package/src/render/index.ts +312 -0
  66. package/src/render/loaders.ts +67 -0
  67. package/src/render/shell.ts +128 -0
  68. package/src/render/template.ts +54 -0
  69. package/src/router.ts +234 -0
  70. package/src/runtime-env.ts +6 -0
  71. package/src/utils.ts +68 -0
package/src/router.ts ADDED
@@ -0,0 +1,234 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readdir } from "node:fs/promises";
3
+ import { join, parse } from "node:path";
4
+ import { type AnyElysia, Elysia } from "elysia";
5
+ import type { AnySchema } from "elysia/types";
6
+ import type { RuntimePage, RuntimeRoute } from "./client.ts";
7
+ import { type CompileContext, getCompileContext } from "./internal.ts";
8
+ import { handleISR, prerenderSSG, renderSSR } from "./render/index.ts";
9
+ import {
10
+ collectRouteChainFromRoute,
11
+ isFurinPage,
12
+ isFurinRoute,
13
+ validateRouteChain,
14
+ } from "./utils.ts";
15
+
16
+ export interface ResolvedRoute {
17
+ isrCache?: { html: string; generatedAt: number; revalidate: number };
18
+ mode: "ssr" | "ssg" | "isr";
19
+ page: RuntimePage;
20
+ path: string;
21
+ pattern: string;
22
+ routeChain: RuntimeRoute[];
23
+ ssgHtml?: string;
24
+ }
25
+
26
+ export interface RootLayout {
27
+ path: string;
28
+ route: RuntimeRoute;
29
+ }
30
+
31
+ export function createRoutePlugin(route: ResolvedRoute, root: RootLayout): AnyElysia {
32
+ const { pattern, mode, routeChain } = route;
33
+
34
+ const plugins: AnyElysia[] = [];
35
+
36
+ const allParams = routeChain.find((r) => r.params)?.params;
37
+ const allQuery = routeChain.find((r) => r.query)?.query;
38
+ if (allParams || allQuery) {
39
+ plugins.push(
40
+ new Elysia().guard({
41
+ params: allParams as AnySchema,
42
+ query: allQuery as AnySchema,
43
+ })
44
+ );
45
+ }
46
+
47
+ plugins.push(
48
+ new Elysia().get(pattern, async (ctx) => {
49
+ switch (mode) {
50
+ case "ssg": {
51
+ ctx.set.headers["content-type"] = "text/html; charset=utf-8";
52
+ ctx.set.headers["cache-control"] = "public, max-age=0, must-revalidate";
53
+ const origin = new URL(ctx.request.url).origin;
54
+ return await prerenderSSG(route, ctx.params, root, origin);
55
+ }
56
+
57
+ case "isr":
58
+ return handleISR(route, ctx, root);
59
+
60
+ default:
61
+ return renderSSR(route, ctx, root);
62
+ }
63
+ })
64
+ );
65
+
66
+ return plugins.reduce((app, plugin) => app.use(plugin), new Elysia());
67
+ }
68
+
69
+ export async function scanRootLayout(pagesDir: string): Promise<RootLayout> {
70
+ const rootPath = `${pagesDir}/root.tsx`;
71
+ const ctx = getCompileContext();
72
+ if (!(existsSync(rootPath) || ctx?.modules[rootPath])) {
73
+ throw new Error("[furin] root.tsx: not found.");
74
+ }
75
+
76
+ const mod = (ctx?.modules[rootPath] ?? (await import(rootPath))) as Record<string, unknown>;
77
+ const rootExport = mod.route ?? mod.default;
78
+ if (!(rootExport && isFurinRoute(rootExport))) {
79
+ throw new Error("[furin] root.tsx: createRoute() export not found.");
80
+ }
81
+
82
+ if (!rootExport.layout) {
83
+ throw new Error("[furin] root.tsx: createRoute() has no layout.");
84
+ }
85
+ return { path: rootPath, route: rootExport };
86
+ }
87
+
88
+ async function collectPageFilePaths(dir: string): Promise<string[]> {
89
+ const files: string[] = [];
90
+
91
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
92
+ const absolutePath = join(dir, entry.name);
93
+
94
+ if (entry.isDirectory()) {
95
+ files.push(...(await collectPageFilePaths(absolutePath)));
96
+ continue;
97
+ }
98
+
99
+ if (entry.isFile()) {
100
+ files.push(absolutePath);
101
+ }
102
+ }
103
+
104
+ return files;
105
+ }
106
+
107
+ async function scanPageFiles(pagesDir: string, root: RootLayout): Promise<ResolvedRoute[]> {
108
+ const routes: ResolvedRoute[] = [];
109
+
110
+ for (const absolutePath of await collectPageFilePaths(pagesDir)) {
111
+ if (![".tsx", ".ts", ".jsx", ".js"].some((ext) => absolutePath.endsWith(ext))) {
112
+ continue;
113
+ }
114
+
115
+ const relativePath = absolutePath.replace(`${pagesDir}/`, "");
116
+ const fileName = parse(relativePath).name;
117
+
118
+ // Skip root.tsx, and files starting with _
119
+ if (fileName.startsWith("_") || fileName === "root") {
120
+ continue;
121
+ }
122
+
123
+ const ctx = getCompileContext();
124
+ const pageMod = (ctx?.modules[absolutePath] ?? (await import(absolutePath))) as {
125
+ default: RuntimePage;
126
+ };
127
+ const page: RuntimePage = pageMod.default;
128
+ if (!isFurinPage(page)) {
129
+ throw new Error(`[furin] ${relativePath}: no valid createRoute().page() export found`);
130
+ }
131
+
132
+ const routeChain = collectRouteChainFromRoute(page._route as RuntimeRoute);
133
+
134
+ validateRouteChain(routeChain, root.route, relativePath);
135
+
136
+ routes.push({
137
+ pattern: filePathToPattern(relativePath),
138
+ page,
139
+ path: absolutePath,
140
+ routeChain,
141
+ mode: resolveMode(page, routeChain),
142
+ });
143
+ }
144
+
145
+ return routes;
146
+ }
147
+
148
+ export async function scanPages(pagesDir: string): Promise<{
149
+ root: RootLayout;
150
+ routes: ResolvedRoute[];
151
+ }> {
152
+ const ctx = getCompileContext();
153
+ if (ctx) {
154
+ return loadProdRoutes(ctx);
155
+ }
156
+ const root = await scanRootLayout(pagesDir);
157
+ const routes = await scanPageFiles(pagesDir, root);
158
+ return { root, routes };
159
+ }
160
+
161
+ export function loadProdRoutes(ctx: CompileContext): {
162
+ root: RootLayout;
163
+ routes: ResolvedRoute[];
164
+ } {
165
+ const rootMod = ctx.modules[ctx.rootPath] as Record<string, unknown>;
166
+ const rootExport = rootMod.route ?? rootMod.default;
167
+ if (!(rootExport && isFurinRoute(rootExport) && rootExport.layout)) {
168
+ throw new Error("[furin] root.tsx: createRoute() with layout not found in CompileContext.");
169
+ }
170
+ const root: RootLayout = { path: ctx.rootPath, route: rootExport };
171
+
172
+ const routes: ResolvedRoute[] = [];
173
+ for (const { pattern, path, mode } of ctx.routes) {
174
+ const pageMod = ctx.modules[path] as { default: RuntimePage };
175
+ const page: RuntimePage = pageMod.default;
176
+ if (!isFurinPage(page)) {
177
+ throw new Error(`[furin] ${path}: invalid page module in CompileContext.`);
178
+ }
179
+ const routeChain = collectRouteChainFromRoute(page._route as RuntimeRoute);
180
+ validateRouteChain(routeChain, root.route, path);
181
+ routes.push({ pattern, page, path, routeChain, mode });
182
+ }
183
+
184
+ return { root, routes };
185
+ }
186
+
187
+ export function resolveMode(page: RuntimePage, routeChain: RuntimeRoute[]): "ssr" | "ssg" | "isr" {
188
+ const routeConfig = page._route;
189
+
190
+ if (routeConfig.mode) {
191
+ return routeConfig.mode;
192
+ }
193
+
194
+ const hasLoader = routeChain.some((r) => r.loader) || !!page.loader;
195
+
196
+ if (!hasLoader) {
197
+ return "ssg";
198
+ }
199
+
200
+ if (routeConfig.revalidate && routeConfig.revalidate > 0) {
201
+ return "isr";
202
+ }
203
+
204
+ return "ssr";
205
+ }
206
+
207
+ export function filePathToPattern(path: string): string {
208
+ const parts = path.replaceAll("\\", "/").split("/");
209
+ const segments: string[] = [];
210
+
211
+ for (const part of parts) {
212
+ const name = parse(part).name;
213
+
214
+ if (name === "index") {
215
+ continue;
216
+ }
217
+
218
+ if (name.startsWith("[") && name.endsWith("]")) {
219
+ const inner = name.slice(1, -1);
220
+
221
+ if (inner.startsWith("...")) {
222
+ segments.push("*");
223
+ continue;
224
+ }
225
+
226
+ segments.push(`:${inner}`);
227
+ continue;
228
+ }
229
+
230
+ segments.push(name);
231
+ }
232
+
233
+ return `/${segments.join("/")}`;
234
+ }
@@ -0,0 +1,6 @@
1
+ export let IS_DEV = process.env.NODE_ENV !== "production";
2
+
3
+ /** @internal test-only — overrides IS_DEV via live binding */
4
+ export function __setDevMode(val: boolean): void {
5
+ IS_DEV = val;
6
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,68 @@
1
+ import type { RuntimePage, RuntimeRoute } from "./client";
2
+
3
+ export function isFurinPage(value: unknown): value is RuntimePage {
4
+ return (
5
+ typeof value === "object" &&
6
+ value !== null &&
7
+ "__type" in value &&
8
+ (value as { __type: unknown }).__type === "FURIN_PAGE"
9
+ );
10
+ }
11
+
12
+ export function isFurinRoute(value: unknown): value is RuntimeRoute {
13
+ return (
14
+ typeof value === "object" &&
15
+ value !== null &&
16
+ "__type" in value &&
17
+ (value as { __type: unknown }).__type === "FURIN_ROUTE"
18
+ );
19
+ }
20
+
21
+ export function collectRouteChainFromRoute(route: RuntimeRoute): RuntimeRoute[] {
22
+ const chain: RuntimeRoute[] = [];
23
+ let current: RuntimeRoute | undefined = route;
24
+
25
+ while (current) {
26
+ chain.unshift(current);
27
+ current = current.parent;
28
+ }
29
+
30
+ return chain;
31
+ }
32
+
33
+ export function hasCycle(route: RuntimeRoute): boolean {
34
+ const visited = new Set<RuntimeRoute>();
35
+ let current: RuntimeRoute | undefined = route;
36
+
37
+ while (current) {
38
+ if (visited.has(current)) {
39
+ return true;
40
+ }
41
+ visited.add(current);
42
+ current = current.parent;
43
+ }
44
+
45
+ return false;
46
+ }
47
+
48
+ export function validateRouteChain(
49
+ chain: RuntimeRoute[],
50
+ root: RuntimeRoute,
51
+ pagePath?: string
52
+ ): void {
53
+ const hasRoot = chain.some((r) => r === root);
54
+
55
+ if (!hasRoot) {
56
+ const location = pagePath ? `in ${pagePath}` : "";
57
+ throw new Error(
58
+ `[furin] Page ${location} must inherit from root route. ` +
59
+ 'Add: import { route } from "./root"; and use route.page() or set parent: route'
60
+ );
61
+ }
62
+
63
+ for (const route of chain) {
64
+ if (hasCycle(route)) {
65
+ throw new Error("[furin] Cycle detected in route chain. A route cannot be its own ancestor.");
66
+ }
67
+ }
68
+ }