@fluixi/start 0.1.0-alpha.73 → 0.1.0-alpha.75

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 (123) hide show
  1. package/dist/adapter.cjs +750 -0
  2. package/dist/adapter.d.ts +9 -9
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/adapter.js +10 -10
  5. package/dist/adapter.mjs +719 -0
  6. package/dist/adapters/entry.cjs +122 -0
  7. package/dist/adapters/entry.js +1 -1
  8. package/dist/adapters/entry.mjs +95 -0
  9. package/dist/adapters/platforms.cjs +311 -0
  10. package/dist/adapters/platforms.d.ts +2 -2
  11. package/dist/adapters/platforms.js +8 -8
  12. package/dist/adapters/platforms.mjs +284 -0
  13. package/dist/api-HD7KU3BS.mjs +114 -0
  14. package/dist/api.cjs +145 -0
  15. package/dist/api.js +1 -1
  16. package/dist/api.mjs +116 -0
  17. package/dist/commands/api-HD7KU3BS.mjs +114 -0
  18. package/dist/commands/api-PBT4QZ34.mjs +16 -0
  19. package/dist/commands/api-PJL37LV3.mjs +17 -0
  20. package/dist/commands/build.cjs +877 -0
  21. package/dist/commands/build.js +6 -6
  22. package/dist/commands/build.mjs +717 -0
  23. package/dist/commands/chunk-GFQ4MUIC.mjs +117 -0
  24. package/dist/commands/chunk-MTM6M3TC.mjs +115 -0
  25. package/dist/commands/dev.cjs +670 -0
  26. package/dist/commands/dev.js +3 -3
  27. package/dist/commands/dev.mjs +512 -0
  28. package/dist/commands/index.cjs +1145 -0
  29. package/dist/commands/index.d.ts +1 -1
  30. package/dist/commands/index.js +1 -1
  31. package/dist/commands/index.mjs +982 -0
  32. package/dist/commands/prerender.cjs +404 -0
  33. package/dist/commands/prerender.d.ts +1 -1
  34. package/dist/commands/prerender.js +2 -2
  35. package/dist/commands/prerender.mjs +377 -0
  36. package/dist/commands/start.cjs +463 -0
  37. package/dist/commands/start.d.ts +1 -1
  38. package/dist/commands/start.js +2 -2
  39. package/dist/commands/start.mjs +428 -0
  40. package/dist/config.cjs +150 -0
  41. package/dist/config.d.ts +24 -7
  42. package/dist/config.d.ts.map +1 -1
  43. package/dist/config.js +8 -6
  44. package/dist/config.mjs +114 -0
  45. package/dist/define-config.cjs +32 -0
  46. package/dist/define-config.d.ts +12 -0
  47. package/dist/define-config.d.ts.map +1 -0
  48. package/dist/define-config.js +3 -0
  49. package/dist/define-config.mjs +7 -0
  50. package/dist/di.cjs +200 -0
  51. package/dist/di.d.ts +4 -4
  52. package/dist/di.d.ts.map +1 -1
  53. package/dist/di.js +9 -9
  54. package/dist/di.mjs +169 -0
  55. package/dist/document.cjs +115 -0
  56. package/dist/document.d.ts +1 -1
  57. package/dist/document.js +1 -1
  58. package/dist/document.mjs +86 -0
  59. package/dist/generated-api.cjs +43 -0
  60. package/dist/generated-api.js +1 -1
  61. package/dist/generated-api.mjs +18 -0
  62. package/dist/generated-server-fns.cjs +43 -0
  63. package/dist/generated-server-fns.mjs +18 -0
  64. package/dist/handler-core.cjs +211 -0
  65. package/dist/handler-core.d.ts +2 -2
  66. package/dist/handler-core.js +2 -2
  67. package/dist/handler-core.mjs +185 -0
  68. package/dist/handler.cjs +182 -0
  69. package/dist/handler.d.ts +3 -3
  70. package/dist/handler.js +3 -3
  71. package/dist/handler.mjs +152 -0
  72. package/dist/head.cjs +25 -0
  73. package/dist/head.d.ts +1 -1
  74. package/dist/head.js +1 -1
  75. package/dist/head.mjs +4 -0
  76. package/dist/image-loader.cjs +37 -0
  77. package/dist/image-loader.d.ts +25 -0
  78. package/dist/image-loader.d.ts.map +1 -0
  79. package/dist/image-loader.js +18 -0
  80. package/dist/image-loader.mjs +12 -0
  81. package/dist/image.cjs +151 -0
  82. package/dist/image.d.ts +2 -19
  83. package/dist/image.d.ts.map +1 -1
  84. package/dist/image.js +5 -21
  85. package/dist/image.mjs +126 -0
  86. package/dist/index.cjs +303 -0
  87. package/dist/index.d.ts +28 -8
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +33 -17
  90. package/dist/index.mjs +262 -0
  91. package/dist/interceptors.cjs +72 -0
  92. package/dist/interceptors.js +1 -1
  93. package/dist/interceptors.mjs +44 -0
  94. package/dist/internal.cjs +393 -0
  95. package/dist/internal.d.ts +6 -6
  96. package/dist/internal.d.ts.map +1 -1
  97. package/dist/internal.js +23 -15
  98. package/dist/internal.mjs +223 -0
  99. package/dist/middleware.cjs +58 -0
  100. package/dist/middleware.d.ts +3 -3
  101. package/dist/middleware.d.ts.map +1 -1
  102. package/dist/middleware.js +1 -1
  103. package/dist/middleware.mjs +31 -0
  104. package/dist/preload.cjs +42 -0
  105. package/dist/preload.js +1 -1
  106. package/dist/preload.mjs +18 -0
  107. package/dist/router.cjs +25 -0
  108. package/dist/router.d.ts +3 -3
  109. package/dist/router.js +3 -3
  110. package/dist/router.mjs +4 -0
  111. package/dist/server-fn-setup.cjs +46 -0
  112. package/dist/server-fn-setup.d.ts +1 -1
  113. package/dist/server-fn-setup.js +2 -2
  114. package/dist/server-fn-setup.mjs +22 -0
  115. package/dist/server-fn.cjs +109 -0
  116. package/dist/server-fn.d.ts +2 -2
  117. package/dist/server-fn.js +2 -2
  118. package/dist/server-fn.mjs +79 -0
  119. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  120. package/dist/ui.cjs +123 -0
  121. package/dist/ui.js +1 -1
  122. package/dist/ui.mjs +93 -0
  123. package/package.json +63 -25
@@ -0,0 +1,284 @@
1
+ // src/adapters/platforms.ts
2
+ import { mkdir, writeFile, readFile as readFile2, cp, readdir } from "node:fs/promises";
3
+ import { existsSync as existsSync2 } from "node:fs";
4
+ import { join as join2, relative } from "node:path";
5
+
6
+ // src/adapter.ts
7
+ import { readFile } from "node:fs/promises";
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { resolve, join, dirname } from "node:path";
10
+
11
+ // src/document.ts
12
+ import { extractHeadMarker } from "@fluixi/head";
13
+
14
+ // src/preload.ts
15
+ import { matchRoutes } from "@fluixi/core/router";
16
+
17
+ // src/adapter.ts
18
+ async function readTemplate(clientDir, serverDir) {
19
+ const stashed = join(serverDir, "template.html");
20
+ return readFile(existsSync(stashed) ? stashed : join(clientDir, "index.html"), "utf-8");
21
+ }
22
+
23
+ // src/adapters/entry.ts
24
+ function preamble({ serverEntry, middleware, template, ssr, mountId, routeAssets }) {
25
+ return [
26
+ `import { createHandlerFrom } from '@fluixi/start/handler-core';`,
27
+ `import { createPreloader } from '@fluixi/start/preload';`,
28
+ `import * as server from ${JSON.stringify(serverEntry)};`,
29
+ middleware ? `import * as middleware from ${JSON.stringify(middleware)};` : "",
30
+ ``,
31
+ `const template = ${JSON.stringify(template)};`,
32
+ // Inlined for the same reason as the template: this runs as an uploaded file, with
33
+ // no dist/server beside it to read.
34
+ `const routeAssets = ${JSON.stringify(routeAssets)};`,
35
+ `const handler = createHandlerFrom({`,
36
+ ` template,`,
37
+ ` mod: server,`,
38
+ ` cfg: { ssr: ${ssr}, mountId: ${JSON.stringify(mountId)} },`,
39
+ ` preload: createPreloader(routeAssets),`,
40
+ middleware ? ` middleware,` : "",
41
+ `});`
42
+ ].filter(Boolean).join("\n");
43
+ }
44
+ function cloudflareEntry(source) {
45
+ return `${preamble(source)}
46
+
47
+ const prerendered = new Set(${JSON.stringify(source.prerendered)});
48
+
49
+ export default {
50
+ async fetch(request, env, ctx) {
51
+ // Static first, but only for what is static. ASSETS answers "/" with index.html
52
+ // whether that is a prerendered page or the shell, so asking about every path
53
+ // costs an SSR app its home page. Extension means a file, prerendered means a
54
+ // page, the rest is the handler's: the rule \`withStaticFiles\` already uses.
55
+ const { pathname } = new URL(request.url);
56
+ if (env && env.ASSETS && (prerendered.has(pathname) || /\\.[^/]+$/.test(pathname))) {
57
+ const asset = await env.ASSETS.fetch(request.clone());
58
+ if (asset.status !== 404) return asset;
59
+ }
60
+ return handler(request, { env, ctx });
61
+ },
62
+ };
63
+ `;
64
+ }
65
+ function netlifyEntry(source) {
66
+ return `${preamble(source)}
67
+
68
+ export default async (request, context) => handler(request, { context });
69
+
70
+ export const config = { path: '/*', preferStatic: true };
71
+ `;
72
+ }
73
+ function vercelEntry(source) {
74
+ return `${preamble(source)}
75
+
76
+ export default async (req, res) => {
77
+ const proto = req.headers['x-forwarded-proto'] || 'https';
78
+ const host = req.headers['x-forwarded-host'] || req.headers.host;
79
+
80
+ const headers = new Headers();
81
+ for (const [name, value] of Object.entries(req.headers)) {
82
+ if (Array.isArray(value)) for (const one of value) headers.append(name, one);
83
+ else if (value !== undefined) headers.set(name, value);
84
+ }
85
+
86
+ // A body has to be read off the stream before it can be handed to Request; only
87
+ // GET and HEAD are guaranteed not to have one.
88
+ let body;
89
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
90
+ const chunks = [];
91
+ for await (const chunk of req) chunks.push(chunk);
92
+ if (chunks.length) body = new Blob(chunks);
93
+ }
94
+
95
+ const response = await handler(
96
+ new Request(new URL(req.url, \`\${proto}://\${host}\`), { method: req.method, headers, body }),
97
+ );
98
+
99
+ res.statusCode = response.status;
100
+ // set-cookie is the one header that legitimately repeats; folding it into one line
101
+ // breaks the cookies.
102
+ for (const [name, value] of response.headers) {
103
+ if (name.toLowerCase() !== 'set-cookie') res.setHeader(name, value);
104
+ }
105
+ const cookies = response.headers.getSetCookie?.() ?? [];
106
+ if (cookies.length) res.setHeader('set-cookie', cookies);
107
+
108
+ if (response.body) for await (const chunk of response.body) res.write(chunk);
109
+ res.end();
110
+ };
111
+ `;
112
+ }
113
+
114
+ // src/adapters/platforms.ts
115
+ async function entrySource(ctx, from, prerendered) {
116
+ const template = await readTemplate(ctx.clientDir, ctx.serverDir);
117
+ const assetFile = join2(ctx.serverDir, "route-assets.json");
118
+ const routeAssets = existsSync2(assetFile) ? JSON.parse(await readFile2(assetFile, "utf-8")) : {};
119
+ const middleware = join2(ctx.serverDir, "middleware.js");
120
+ const rel = (file) => {
121
+ const path = join2(ctx.serverDir, file).replace(/\\/g, "/");
122
+ return path.startsWith("/") ? path : `./${path}`;
123
+ };
124
+ return {
125
+ serverEntry: rel("entry-server.js"),
126
+ middleware: existsSync2(middleware) ? rel("middleware.js") : null,
127
+ template,
128
+ ssr: ctx.cfg.ssr,
129
+ mountId: ctx.cfg.mountId,
130
+ prerendered,
131
+ routeAssets
132
+ };
133
+ }
134
+ function indexIsPage(template, mountId) {
135
+ const mount = new RegExp(`<([a-z-]+)[^>]*\\sid=["']${mountId}["'][^>]*>`, "i").exec(template);
136
+ if (!mount) return false;
137
+ return !template.slice(mount.index + mount[0].length).trimStart().startsWith("</");
138
+ }
139
+ function skipWhenStatic(ctx, name) {
140
+ if (ctx.hasServer) return false;
141
+ console.log(` fluixi: no server entry — ${name} gets the static output alone.`);
142
+ return true;
143
+ }
144
+ async function appendRedirect(clientDir, rule) {
145
+ const file = join2(clientDir, "_redirects");
146
+ const existing = await readFile2(file, "utf-8").catch(() => "");
147
+ if (existing.includes(rule)) return;
148
+ const separator = existing && !existing.endsWith("\n") ? "\n" : "";
149
+ await writeFile(file, `${existing}${separator}${rule}
150
+ `);
151
+ }
152
+ var SPA_FALLBACK = "/* /index.html 200";
153
+ var CF_MAX_RULES = 100;
154
+ async function prerenderedRoutes(dir, mountId) {
155
+ const out = [];
156
+ const walk = async (current) => {
157
+ for (const entry of await readdir(current, { withFileTypes: true })) {
158
+ const path = join2(current, entry.name);
159
+ if (entry.isDirectory()) {
160
+ if (entry.name === "assets" || entry.name.startsWith(".")) continue;
161
+ await walk(path);
162
+ } else if (entry.name.endsWith(".html")) {
163
+ const rel = relative(dir, path).replace(/\\/g, "/").replace(/index\.html$/, "").replace(/\.html$/, "");
164
+ out.push("/" + rel.replace(/\/$/, ""));
165
+ }
166
+ }
167
+ };
168
+ await walk(dir);
169
+ const index = await readFile2(join2(dir, "index.html"), "utf-8").catch(() => "");
170
+ const root = indexIsPage(index, mountId);
171
+ return out.filter((r) => r !== "/" || root).sort();
172
+ }
173
+ var cloudflareAdapter = {
174
+ name: "cloudflare",
175
+ bundle: "inline",
176
+ serve({ handler }) {
177
+ return { fetch: handler };
178
+ },
179
+ async build(ctx) {
180
+ if (skipWhenStatic(ctx, "cloudflare")) return;
181
+ const out = join2(ctx.clientDir, "_worker.js");
182
+ const prerendered = await prerenderedRoutes(ctx.clientDir, ctx.cfg.mountId);
183
+ await ctx.bundleEntry(cloudflareEntry(await entrySource(ctx, out, prerendered)), out);
184
+ const base = ["/assets/*", "/_worker.js"];
185
+ const room = CF_MAX_RULES - base.length;
186
+ const exclude = [...base, ...prerendered.slice(0, room)];
187
+ if (prerendered.length > room) {
188
+ console.warn(
189
+ ` fluixi: ${prerendered.length} prerendered routes exceed Cloudflare's ${CF_MAX_RULES}-rule limit for _routes.json; ${prerendered.length - room} will invoke the worker to serve a static file.`
190
+ );
191
+ }
192
+ await writeFile(
193
+ join2(ctx.clientDir, "_routes.json"),
194
+ JSON.stringify({ version: 1, include: ["/*"], exclude }, null, 2) + "\n"
195
+ );
196
+ }
197
+ };
198
+ var netlifyAdapter = {
199
+ name: "netlify",
200
+ bundle: "inline",
201
+ serve({ handler }) {
202
+ return { fetch: handler };
203
+ },
204
+ async build(ctx) {
205
+ if (skipWhenStatic(ctx, "netlify")) return appendRedirect(ctx.clientDir, SPA_FALLBACK);
206
+ const dir = join2(ctx.root, ".netlify", "functions-internal");
207
+ await mkdir(dir, { recursive: true });
208
+ const out = join2(dir, "fluixi-server.mjs");
209
+ const prerendered = await prerenderedRoutes(ctx.clientDir, ctx.cfg.mountId);
210
+ await ctx.bundleEntry(netlifyEntry(await entrySource(ctx, out, prerendered)), out);
211
+ await writeFile(
212
+ join2(dir, "fluixi-server.json"),
213
+ JSON.stringify({ config: { path: "/*", preferStatic: true }, version: 1 }, null, 2) + "\n"
214
+ );
215
+ if (!prerendered.includes("/")) {
216
+ await appendRedirect(ctx.clientDir, "/ /.netlify/functions/fluixi-server 200!");
217
+ }
218
+ }
219
+ };
220
+ var vercelAdapter = {
221
+ name: "vercel",
222
+ bundle: "inline",
223
+ serve({ handler }) {
224
+ return { fetch: handler };
225
+ },
226
+ async build(ctx) {
227
+ const output = join2(ctx.root, ".vercel", "output");
228
+ await mkdir(output, { recursive: true });
229
+ await cp(ctx.clientDir, join2(output, "static"), { recursive: true });
230
+ if (skipWhenStatic(ctx, "vercel")) {
231
+ await writeFile(
232
+ join2(output, "config.json"),
233
+ JSON.stringify(
234
+ {
235
+ version: 3,
236
+ // The filesystem phase alone answers 404 for a client route, since nothing
237
+ // is on disk at that path; the catch-all is what makes it a SPA.
238
+ routes: [{ handle: "filesystem" }, { src: "/(.*)", dest: "/index.html" }]
239
+ },
240
+ null,
241
+ 2
242
+ ) + "\n"
243
+ );
244
+ return;
245
+ }
246
+ const fn = join2(output, "functions", "index.func");
247
+ await mkdir(fn, { recursive: true });
248
+ const out = join2(fn, "index.mjs");
249
+ const prerendered = await prerenderedRoutes(ctx.clientDir, ctx.cfg.mountId);
250
+ await ctx.bundleEntry(vercelEntry(await entrySource(ctx, out, prerendered)), out);
251
+ await writeFile(
252
+ join2(fn, ".vc-config.json"),
253
+ JSON.stringify(
254
+ { runtime: "nodejs20.x", handler: "index.mjs", launcherType: "Nodejs", shouldAddHelpers: false },
255
+ null,
256
+ 2
257
+ ) + "\n"
258
+ );
259
+ await writeFile(
260
+ join2(output, "config.json"),
261
+ JSON.stringify(
262
+ {
263
+ version: 3,
264
+ routes: [
265
+ // The filesystem holds a shell at `/index.html`, so `/` has to be claimed
266
+ // before that phase or it is served unrendered. Skipped when `/` really is
267
+ // a prerendered page, which the filesystem should answer.
268
+ ...prerendered.includes("/") ? [] : [{ src: "/", dest: "/index" }],
269
+ // A prerendered page or an asset wins; only a miss reaches the function.
270
+ { handle: "filesystem" },
271
+ { src: "/(.*)", dest: "/index" }
272
+ ]
273
+ },
274
+ null,
275
+ 2
276
+ ) + "\n"
277
+ );
278
+ }
279
+ };
280
+ export {
281
+ cloudflareAdapter,
282
+ netlifyAdapter,
283
+ vercelAdapter
284
+ };
@@ -0,0 +1,114 @@
1
+ // src/api.ts
2
+ import { readdirSync, statSync } from "node:fs";
3
+ import { resolve as resolvePath, relative as relativePath, join } from "node:path";
4
+ var API_VMOD = "virtual:fluixi-api";
5
+ var METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
6
+ function derivePattern(rel, prefix = "/api") {
7
+ const segs = rel.split("/").filter(Boolean);
8
+ if (segs[segs.length - 1] === "index") segs.pop();
9
+ const keys = [];
10
+ let rank = 0;
11
+ const parts = segs.map((seg) => {
12
+ const catchAll = seg.match(/^\[\.\.\.(.+)\]$/);
13
+ if (catchAll) {
14
+ keys.push(catchAll[1]);
15
+ return "(.+)";
16
+ }
17
+ const param = seg.match(/^\[(.+)\]$/);
18
+ if (param) {
19
+ keys.push(param[1]);
20
+ rank += 1;
21
+ return "([^/]+)";
22
+ }
23
+ rank += 10;
24
+ return seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
25
+ });
26
+ return { source: `^${prefix}${parts.length ? "/" + parts.join("/") : ""}/?$`, keys, rank };
27
+ }
28
+ function isApiPath(pathname, prefix = "/api") {
29
+ return pathname === prefix || pathname.startsWith(prefix + "/");
30
+ }
31
+ function matchApiRoutes(routes, pathname) {
32
+ for (const r of [...routes].sort((a, b) => b.rank - a.rank)) {
33
+ const m = new RegExp(r.source).exec(pathname);
34
+ if (!m) continue;
35
+ const params = {};
36
+ r.keys.forEach((k, i) => params[k] = decodeURIComponent(m[i + 1] ?? ""));
37
+ return { mod: r.mod, params };
38
+ }
39
+ return null;
40
+ }
41
+ async function handleApi(routes, request, prefix = "/api") {
42
+ const pathname = new URL(request.url).pathname;
43
+ const match = matchApiRoutes(routes, pathname);
44
+ if (!match) return new Response("Not Found", { status: 404 });
45
+ const fn = match.mod[request.method];
46
+ if (typeof fn !== "function") {
47
+ const allow = METHODS.filter((m) => typeof match.mod[m] === "function").join(", ");
48
+ return new Response("Method Not Allowed", { status: 405, headers: allow ? { allow } : void 0 });
49
+ }
50
+ const out = await fn(request, { params: match.params });
51
+ if (out instanceof Response) return out;
52
+ return new Response(JSON.stringify(out ?? null), { headers: { "content-type": "application/json; charset=utf-8" } });
53
+ }
54
+ function scanFiles(dir, base = dir) {
55
+ let out = [];
56
+ let entries = [];
57
+ try {
58
+ entries = readdirSync(dir);
59
+ } catch {
60
+ return out;
61
+ }
62
+ for (const name of entries) {
63
+ const full = join(dir, name);
64
+ if (statSync(full).isDirectory()) out = out.concat(scanFiles(full, base));
65
+ else if (/\.(ts|tsx|js|mjs)$/.test(name) && !/\.(spec|test)\./.test(name)) out.push(full);
66
+ }
67
+ return out;
68
+ }
69
+ function apiRoutesVitePlugin(opts) {
70
+ let root = process.cwd();
71
+ const prefix = opts.prefix ?? "/api";
72
+ const id = "\0" + API_VMOD;
73
+ return {
74
+ name: "fluixi-api",
75
+ // Must beat node resolution: '@fluixi/start/api-routes' is a real file in this package,
76
+ // so without `pre` Vite resolves the typed stub and the generated table never loads ,
77
+ // the app builds and every API route 404s.
78
+ enforce: "pre",
79
+ configResolved(config) {
80
+ root = config.root || root;
81
+ },
82
+ resolveId(source) {
83
+ return source === API_VMOD || source === "@fluixi/start/api-routes" ? id : null;
84
+ },
85
+ load(source) {
86
+ if (source !== id) return null;
87
+ const dir = resolvePath(root, opts.dir);
88
+ const files = scanFiles(dir);
89
+ const imports = [];
90
+ const entries = [];
91
+ files.forEach((file, i) => {
92
+ const rel = relativePath(dir, file).replace(/\.(ts|tsx|js|mjs)$/, "").split(/[\\/]/).join("/");
93
+ const { source: src, keys, rank } = derivePattern(rel, prefix);
94
+ imports.push(`import * as _${i} from ${JSON.stringify(file)};`);
95
+ entries.push(`{ source: ${JSON.stringify(src)}, keys: ${JSON.stringify(keys)}, rank: ${rank}, mod: _${i} }`);
96
+ });
97
+ return `import { isApiPath, handleApi } from '@fluixi/start/api';
98
+ ${imports.join("\n")}
99
+ const routes = [${entries.join(",")}];
100
+ export const isApiRequest = (request) => isApiPath(new URL(request.url).pathname, ${JSON.stringify(prefix)});
101
+ export const handleApiRequest = (request) => handleApi(routes, request, ${JSON.stringify(prefix)});
102
+ `;
103
+ }
104
+ };
105
+ }
106
+ var API_VIRTUAL_MODULE = API_VMOD;
107
+ export {
108
+ API_VIRTUAL_MODULE,
109
+ apiRoutesVitePlugin,
110
+ derivePattern,
111
+ handleApi,
112
+ isApiPath,
113
+ matchApiRoutes
114
+ };
package/dist/api.cjs ADDED
@@ -0,0 +1,145 @@
1
+ /*! @fluixi/start v0.1.0-alpha.75 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/api.ts
22
+ var api_exports = {};
23
+ __export(api_exports, {
24
+ API_VIRTUAL_MODULE: () => API_VIRTUAL_MODULE,
25
+ apiRoutesVitePlugin: () => apiRoutesVitePlugin,
26
+ derivePattern: () => derivePattern,
27
+ handleApi: () => handleApi,
28
+ isApiPath: () => isApiPath,
29
+ matchApiRoutes: () => matchApiRoutes
30
+ });
31
+ module.exports = __toCommonJS(api_exports);
32
+ var import_node_fs = require("node:fs");
33
+ var import_node_path = require("node:path");
34
+ var API_VMOD = "virtual:fluixi-api";
35
+ var METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
36
+ function derivePattern(rel, prefix = "/api") {
37
+ const segs = rel.split("/").filter(Boolean);
38
+ if (segs[segs.length - 1] === "index") segs.pop();
39
+ const keys = [];
40
+ let rank = 0;
41
+ const parts = segs.map((seg) => {
42
+ const catchAll = seg.match(/^\[\.\.\.(.+)\]$/);
43
+ if (catchAll) {
44
+ keys.push(catchAll[1]);
45
+ return "(.+)";
46
+ }
47
+ const param = seg.match(/^\[(.+)\]$/);
48
+ if (param) {
49
+ keys.push(param[1]);
50
+ rank += 1;
51
+ return "([^/]+)";
52
+ }
53
+ rank += 10;
54
+ return seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
55
+ });
56
+ return { source: `^${prefix}${parts.length ? "/" + parts.join("/") : ""}/?$`, keys, rank };
57
+ }
58
+ function isApiPath(pathname, prefix = "/api") {
59
+ return pathname === prefix || pathname.startsWith(prefix + "/");
60
+ }
61
+ function matchApiRoutes(routes, pathname) {
62
+ for (const r of [...routes].sort((a, b) => b.rank - a.rank)) {
63
+ const m = new RegExp(r.source).exec(pathname);
64
+ if (!m) continue;
65
+ const params = {};
66
+ r.keys.forEach((k, i) => params[k] = decodeURIComponent(m[i + 1] ?? ""));
67
+ return { mod: r.mod, params };
68
+ }
69
+ return null;
70
+ }
71
+ async function handleApi(routes, request, prefix = "/api") {
72
+ const pathname = new URL(request.url).pathname;
73
+ const match = matchApiRoutes(routes, pathname);
74
+ if (!match) return new Response("Not Found", { status: 404 });
75
+ const fn = match.mod[request.method];
76
+ if (typeof fn !== "function") {
77
+ const allow = METHODS.filter((m) => typeof match.mod[m] === "function").join(", ");
78
+ return new Response("Method Not Allowed", { status: 405, headers: allow ? { allow } : void 0 });
79
+ }
80
+ const out = await fn(request, { params: match.params });
81
+ if (out instanceof Response) return out;
82
+ return new Response(JSON.stringify(out ?? null), { headers: { "content-type": "application/json; charset=utf-8" } });
83
+ }
84
+ function scanFiles(dir, base = dir) {
85
+ let out = [];
86
+ let entries = [];
87
+ try {
88
+ entries = (0, import_node_fs.readdirSync)(dir);
89
+ } catch {
90
+ return out;
91
+ }
92
+ for (const name of entries) {
93
+ const full = (0, import_node_path.join)(dir, name);
94
+ if ((0, import_node_fs.statSync)(full).isDirectory()) out = out.concat(scanFiles(full, base));
95
+ else if (/\.(ts|tsx|js|mjs)$/.test(name) && !/\.(spec|test)\./.test(name)) out.push(full);
96
+ }
97
+ return out;
98
+ }
99
+ function apiRoutesVitePlugin(opts) {
100
+ let root = process.cwd();
101
+ const prefix = opts.prefix ?? "/api";
102
+ const id = "\0" + API_VMOD;
103
+ return {
104
+ name: "fluixi-api",
105
+ // Must beat node resolution: '@fluixi/start/api-routes' is a real file in this package,
106
+ // so without `pre` Vite resolves the typed stub and the generated table never loads ,
107
+ // the app builds and every API route 404s.
108
+ enforce: "pre",
109
+ configResolved(config) {
110
+ root = config.root || root;
111
+ },
112
+ resolveId(source) {
113
+ return source === API_VMOD || source === "@fluixi/start/api-routes" ? id : null;
114
+ },
115
+ load(source) {
116
+ if (source !== id) return null;
117
+ const dir = (0, import_node_path.resolve)(root, opts.dir);
118
+ const files = scanFiles(dir);
119
+ const imports = [];
120
+ const entries = [];
121
+ files.forEach((file, i) => {
122
+ const rel = (0, import_node_path.relative)(dir, file).replace(/\.(ts|tsx|js|mjs)$/, "").split(/[\\/]/).join("/");
123
+ const { source: src, keys, rank } = derivePattern(rel, prefix);
124
+ imports.push(`import * as _${i} from ${JSON.stringify(file)};`);
125
+ entries.push(`{ source: ${JSON.stringify(src)}, keys: ${JSON.stringify(keys)}, rank: ${rank}, mod: _${i} }`);
126
+ });
127
+ return `import { isApiPath, handleApi } from '@fluixi/start/api';
128
+ ${imports.join("\n")}
129
+ const routes = [${entries.join(",")}];
130
+ export const isApiRequest = (request) => isApiPath(new URL(request.url).pathname, ${JSON.stringify(prefix)});
131
+ export const handleApiRequest = (request) => handleApi(routes, request, ${JSON.stringify(prefix)});
132
+ `;
133
+ }
134
+ };
135
+ }
136
+ var API_VIRTUAL_MODULE = API_VMOD;
137
+ // Annotate the CommonJS export names for ESM import in node:
138
+ 0 && (module.exports = {
139
+ API_VIRTUAL_MODULE,
140
+ apiRoutesVitePlugin,
141
+ derivePattern,
142
+ handleApi,
143
+ isApiPath,
144
+ matchApiRoutes
145
+ });
package/dist/api.js CHANGED
@@ -91,7 +91,7 @@ export function apiRoutesVitePlugin(opts) {
91
91
  return {
92
92
  name: 'fluixi-api',
93
93
  // Must beat node resolution: '@fluixi/start/api-routes' is a real file in this package,
94
- // so without `pre` Vite resolves the typed stub and the generated table never loads
94
+ // so without `pre` Vite resolves the typed stub and the generated table never loads ,
95
95
  // the app builds and every API route 404s.
96
96
  enforce: 'pre',
97
97
  configResolved(config) {
package/dist/api.mjs ADDED
@@ -0,0 +1,116 @@
1
+ /*! @fluixi/start v0.1.0-alpha.75 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
+
3
+ // src/api.ts
4
+ import { readdirSync, statSync } from "node:fs";
5
+ import { resolve as resolvePath, relative as relativePath, join } from "node:path";
6
+ var API_VMOD = "virtual:fluixi-api";
7
+ var METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
8
+ function derivePattern(rel, prefix = "/api") {
9
+ const segs = rel.split("/").filter(Boolean);
10
+ if (segs[segs.length - 1] === "index") segs.pop();
11
+ const keys = [];
12
+ let rank = 0;
13
+ const parts = segs.map((seg) => {
14
+ const catchAll = seg.match(/^\[\.\.\.(.+)\]$/);
15
+ if (catchAll) {
16
+ keys.push(catchAll[1]);
17
+ return "(.+)";
18
+ }
19
+ const param = seg.match(/^\[(.+)\]$/);
20
+ if (param) {
21
+ keys.push(param[1]);
22
+ rank += 1;
23
+ return "([^/]+)";
24
+ }
25
+ rank += 10;
26
+ return seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
27
+ });
28
+ return { source: `^${prefix}${parts.length ? "/" + parts.join("/") : ""}/?$`, keys, rank };
29
+ }
30
+ function isApiPath(pathname, prefix = "/api") {
31
+ return pathname === prefix || pathname.startsWith(prefix + "/");
32
+ }
33
+ function matchApiRoutes(routes, pathname) {
34
+ for (const r of [...routes].sort((a, b) => b.rank - a.rank)) {
35
+ const m = new RegExp(r.source).exec(pathname);
36
+ if (!m) continue;
37
+ const params = {};
38
+ r.keys.forEach((k, i) => params[k] = decodeURIComponent(m[i + 1] ?? ""));
39
+ return { mod: r.mod, params };
40
+ }
41
+ return null;
42
+ }
43
+ async function handleApi(routes, request, prefix = "/api") {
44
+ const pathname = new URL(request.url).pathname;
45
+ const match = matchApiRoutes(routes, pathname);
46
+ if (!match) return new Response("Not Found", { status: 404 });
47
+ const fn = match.mod[request.method];
48
+ if (typeof fn !== "function") {
49
+ const allow = METHODS.filter((m) => typeof match.mod[m] === "function").join(", ");
50
+ return new Response("Method Not Allowed", { status: 405, headers: allow ? { allow } : void 0 });
51
+ }
52
+ const out = await fn(request, { params: match.params });
53
+ if (out instanceof Response) return out;
54
+ return new Response(JSON.stringify(out ?? null), { headers: { "content-type": "application/json; charset=utf-8" } });
55
+ }
56
+ function scanFiles(dir, base = dir) {
57
+ let out = [];
58
+ let entries = [];
59
+ try {
60
+ entries = readdirSync(dir);
61
+ } catch {
62
+ return out;
63
+ }
64
+ for (const name of entries) {
65
+ const full = join(dir, name);
66
+ if (statSync(full).isDirectory()) out = out.concat(scanFiles(full, base));
67
+ else if (/\.(ts|tsx|js|mjs)$/.test(name) && !/\.(spec|test)\./.test(name)) out.push(full);
68
+ }
69
+ return out;
70
+ }
71
+ function apiRoutesVitePlugin(opts) {
72
+ let root = process.cwd();
73
+ const prefix = opts.prefix ?? "/api";
74
+ const id = "\0" + API_VMOD;
75
+ return {
76
+ name: "fluixi-api",
77
+ // Must beat node resolution: '@fluixi/start/api-routes' is a real file in this package,
78
+ // so without `pre` Vite resolves the typed stub and the generated table never loads ,
79
+ // the app builds and every API route 404s.
80
+ enforce: "pre",
81
+ configResolved(config) {
82
+ root = config.root || root;
83
+ },
84
+ resolveId(source) {
85
+ return source === API_VMOD || source === "@fluixi/start/api-routes" ? id : null;
86
+ },
87
+ load(source) {
88
+ if (source !== id) return null;
89
+ const dir = resolvePath(root, opts.dir);
90
+ const files = scanFiles(dir);
91
+ const imports = [];
92
+ const entries = [];
93
+ files.forEach((file, i) => {
94
+ const rel = relativePath(dir, file).replace(/\.(ts|tsx|js|mjs)$/, "").split(/[\\/]/).join("/");
95
+ const { source: src, keys, rank } = derivePattern(rel, prefix);
96
+ imports.push(`import * as _${i} from ${JSON.stringify(file)};`);
97
+ entries.push(`{ source: ${JSON.stringify(src)}, keys: ${JSON.stringify(keys)}, rank: ${rank}, mod: _${i} }`);
98
+ });
99
+ return `import { isApiPath, handleApi } from '@fluixi/start/api';
100
+ ${imports.join("\n")}
101
+ const routes = [${entries.join(",")}];
102
+ export const isApiRequest = (request) => isApiPath(new URL(request.url).pathname, ${JSON.stringify(prefix)});
103
+ export const handleApiRequest = (request) => handleApi(routes, request, ${JSON.stringify(prefix)});
104
+ `;
105
+ }
106
+ };
107
+ }
108
+ var API_VIRTUAL_MODULE = API_VMOD;
109
+ export {
110
+ API_VIRTUAL_MODULE,
111
+ apiRoutesVitePlugin,
112
+ derivePattern,
113
+ handleApi,
114
+ isApiPath,
115
+ matchApiRoutes
116
+ };