@fluixi/start 0.1.0-alpha.63 → 0.1.0-alpha.64
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/dist/adapter.d.ts +77 -7
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +131 -44
- package/dist/adapters/entry.d.ts +49 -0
- package/dist/adapters/entry.d.ts.map +1 -0
- package/dist/adapters/entry.js +122 -0
- package/dist/adapters/platforms.d.ts +30 -0
- package/dist/adapters/platforms.d.ts.map +1 -0
- package/dist/adapters/platforms.js +207 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +7 -1
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +73 -4
- package/dist/commands/index.d.ts +14 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +13 -0
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +10 -1
- package/dist/config.d.ts +10 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/document.d.ts +30 -0
- package/dist/document.d.ts.map +1 -0
- package/dist/document.js +100 -0
- package/dist/generated-api.d.ts +5 -0
- package/dist/generated-api.d.ts.map +1 -0
- package/dist/generated-api.js +26 -0
- package/dist/generated-server-fns.d.ts +5 -0
- package/dist/generated-server-fns.d.ts.map +1 -0
- package/dist/generated-server-fns.js +26 -0
- package/dist/handler-core.d.ts +36 -0
- package/dist/handler-core.d.ts.map +1 -0
- package/dist/handler-core.js +40 -0
- package/dist/index.d.ts +5 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -6
- package/dist/internal.d.ts +1 -29
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +10 -102
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +39 -6
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deploy adapters for the three hosts that expect a specific output layout.
|
|
3
|
+
*
|
|
4
|
+
* Each one bundles the same generated entry and then writes whatever the platform
|
|
5
|
+
* reads to route a request. What differs is only that layout — the handler, the
|
|
6
|
+
* dispatch order and the render are shared.
|
|
7
|
+
*
|
|
8
|
+
* All three inline the framework: a worker or a serverless function is a file that
|
|
9
|
+
* gets uploaded, not an install, so nothing may be left to resolve at runtime.
|
|
10
|
+
*/
|
|
11
|
+
import { mkdir, writeFile, readFile, cp, readdir } from 'node:fs/promises';
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
13
|
+
import { join, relative } from 'node:path';
|
|
14
|
+
import { readTemplate } from '../adapter.js';
|
|
15
|
+
import { cloudflareEntry, netlifyEntry, vercelEntry } from './entry.js';
|
|
16
|
+
/** What the generated entry needs to know about this build. */
|
|
17
|
+
async function entrySource(ctx, from, prerendered) {
|
|
18
|
+
const template = await readTemplate(ctx.clientDir, ctx.serverDir);
|
|
19
|
+
const middleware = join(ctx.serverDir, 'middleware.js');
|
|
20
|
+
const rel = (file) => {
|
|
21
|
+
// Node resolves a relative specifier against the importer, and the entry is
|
|
22
|
+
// written to `from`; keep it POSIX so the bundler reads it the same on Windows.
|
|
23
|
+
const path = join(ctx.serverDir, file).replace(/\\/g, '/');
|
|
24
|
+
return path.startsWith('/') ? path : `./${path}`;
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
serverEntry: rel('entry-server.js'),
|
|
28
|
+
middleware: existsSync(middleware) ? rel('middleware.js') : null,
|
|
29
|
+
template,
|
|
30
|
+
ssr: ctx.cfg.ssr,
|
|
31
|
+
mountId: ctx.cfg.mountId,
|
|
32
|
+
prerendered,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Is `dist/client/index.html` a page, or the shell the server renders into?
|
|
37
|
+
*
|
|
38
|
+
* Vite writes the shell at exactly the path a prerendered `/` would occupy, and every
|
|
39
|
+
* one of these platforms serves a matching file before it invokes anything. Left
|
|
40
|
+
* alone, `/` is the one route an SSR app can never render — the CDN answers it with
|
|
41
|
+
* an empty mount element. Every other route is safe: no file exists at `/about`
|
|
42
|
+
* unless we actually prerendered it. So the adapters ask this before deciding who
|
|
43
|
+
* owns `/`.
|
|
44
|
+
*/
|
|
45
|
+
function indexIsPage(template, mountId) {
|
|
46
|
+
const mount = new RegExp(`<([a-z-]+)[^>]*\\sid=["']${mountId}["'][^>]*>`, 'i').exec(template);
|
|
47
|
+
if (!mount)
|
|
48
|
+
return false;
|
|
49
|
+
return !template.slice(mount.index + mount[0].length).trimStart().startsWith('</');
|
|
50
|
+
}
|
|
51
|
+
/** Nothing to wrap: a fully static SPA has no server entry, and that is not a failure. */
|
|
52
|
+
function skipWhenStatic(ctx, name) {
|
|
53
|
+
if (ctx.hasServer)
|
|
54
|
+
return false;
|
|
55
|
+
console.log(` fluixi: no server entry — ${name} gets the static output alone.`);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
/** Cloudflare caps `_routes.json` at 100 rules. */
|
|
59
|
+
const CF_MAX_RULES = 100;
|
|
60
|
+
/**
|
|
61
|
+
* The routes a prerendered page is served at, from the HTML on disk.
|
|
62
|
+
*
|
|
63
|
+
* `/` is in the list only when `index.html` holds a rendered page rather than the
|
|
64
|
+
* shell, because that is the difference between a file the platform should serve and
|
|
65
|
+
* a route the app has to render.
|
|
66
|
+
*/
|
|
67
|
+
async function prerenderedRoutes(dir, mountId) {
|
|
68
|
+
const out = [];
|
|
69
|
+
const walk = async (current) => {
|
|
70
|
+
for (const entry of await readdir(current, { withFileTypes: true })) {
|
|
71
|
+
const path = join(current, entry.name);
|
|
72
|
+
if (entry.isDirectory()) {
|
|
73
|
+
if (entry.name === 'assets' || entry.name.startsWith('.'))
|
|
74
|
+
continue;
|
|
75
|
+
await walk(path);
|
|
76
|
+
}
|
|
77
|
+
else if (entry.name.endsWith('.html')) {
|
|
78
|
+
const rel = relative(dir, path).replace(/\\/g, '/').replace(/index\.html$/, '').replace(/\.html$/, '');
|
|
79
|
+
out.push('/' + rel.replace(/\/$/, ''));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
await walk(dir);
|
|
84
|
+
const index = await readFile(join(dir, 'index.html'), 'utf-8').catch(() => '');
|
|
85
|
+
const root = indexIsPage(index, mountId);
|
|
86
|
+
return out.filter((r) => r !== '/' || root).sort();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Cloudflare Pages. `_worker.js` at the root of the published directory takes every
|
|
90
|
+
* request; `_routes.json` is what decides whether a request costs an invocation.
|
|
91
|
+
*
|
|
92
|
+
* In advanced mode the worker sees everything not excluded — so a prerendered page
|
|
93
|
+
* would wake it just to proxy the file back through the ASSETS binding. On a plan
|
|
94
|
+
* metered by request that is the whole page view budget spent on static files, so
|
|
95
|
+
* every prerendered path is excluded by name.
|
|
96
|
+
*
|
|
97
|
+
* Publish `dist/client`.
|
|
98
|
+
*/
|
|
99
|
+
export const cloudflareAdapter = {
|
|
100
|
+
name: 'cloudflare',
|
|
101
|
+
bundle: 'inline',
|
|
102
|
+
serve({ handler }) {
|
|
103
|
+
return { fetch: handler };
|
|
104
|
+
},
|
|
105
|
+
async build(ctx) {
|
|
106
|
+
if (skipWhenStatic(ctx, 'cloudflare'))
|
|
107
|
+
return;
|
|
108
|
+
const out = join(ctx.clientDir, '_worker.js');
|
|
109
|
+
const prerendered = await prerenderedRoutes(ctx.clientDir, ctx.cfg.mountId);
|
|
110
|
+
await ctx.bundleEntry(cloudflareEntry(await entrySource(ctx, out, prerendered)), out);
|
|
111
|
+
// Everything under /assets is content-hashed and immutable, so it never needs the
|
|
112
|
+
// worker. `_worker.js` itself must be excluded or Pages would route it to itself.
|
|
113
|
+
// `/` is excluded only when it was prerendered — otherwise the worker is the only
|
|
114
|
+
// thing that can answer it, and excluding it would serve the shell instead.
|
|
115
|
+
const base = ['/assets/*', '/_worker.js'];
|
|
116
|
+
const room = CF_MAX_RULES - base.length;
|
|
117
|
+
const exclude = [...base, ...prerendered.slice(0, room)];
|
|
118
|
+
if (prerendered.length > room) {
|
|
119
|
+
// Past the cap the remainder still works — it just wakes the worker to hand
|
|
120
|
+
// back a file. Worth saying, because the symptom is a bill, not an error.
|
|
121
|
+
console.warn(` fluixi: ${prerendered.length} prerendered routes exceed Cloudflare's ${CF_MAX_RULES}-rule ` +
|
|
122
|
+
`limit for _routes.json; ${prerendered.length - room} will invoke the worker to serve a static file.`);
|
|
123
|
+
}
|
|
124
|
+
await writeFile(join(ctx.clientDir, '_routes.json'), JSON.stringify({ version: 1, include: ['/*'], exclude }, null, 2) + '\n');
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Netlify. The function is a v2 fetch handler under `.netlify/functions-internal/`,
|
|
129
|
+
* which is where a framework — as opposed to the user — puts one, so it cannot
|
|
130
|
+
* collide with the app's own `netlify/functions`.
|
|
131
|
+
*
|
|
132
|
+
* Publish `dist/client`.
|
|
133
|
+
*/
|
|
134
|
+
export const netlifyAdapter = {
|
|
135
|
+
name: 'netlify',
|
|
136
|
+
bundle: 'inline',
|
|
137
|
+
serve({ handler }) {
|
|
138
|
+
return { fetch: handler };
|
|
139
|
+
},
|
|
140
|
+
async build(ctx) {
|
|
141
|
+
if (skipWhenStatic(ctx, 'netlify'))
|
|
142
|
+
return;
|
|
143
|
+
const dir = join(ctx.root, '.netlify', 'functions-internal');
|
|
144
|
+
await mkdir(dir, { recursive: true });
|
|
145
|
+
const out = join(dir, 'fluixi-server.mjs');
|
|
146
|
+
const prerendered = await prerenderedRoutes(ctx.clientDir, ctx.cfg.mountId);
|
|
147
|
+
await ctx.bundleEntry(netlifyEntry(await entrySource(ctx, out, prerendered)), out);
|
|
148
|
+
// v2 functions declare their own routing, but the manifest is what makes Netlify
|
|
149
|
+
// load an internal function at all.
|
|
150
|
+
await writeFile(join(dir, 'fluixi-server.json'), JSON.stringify({ config: { path: '/*', preferStatic: true }, version: 1 }, null, 2) + '\n');
|
|
151
|
+
// `preferStatic` is what keeps assets, prerendered pages and files like
|
|
152
|
+
// `favicon.ico` off the function — and it is also what would hand back the shell
|
|
153
|
+
// for `/`. A forced redirect is the one exception the CDN honours over a file, so
|
|
154
|
+
// it claims exactly that path and nothing else. Appended, so an app's own rules in
|
|
155
|
+
// `public/_redirects` still match first.
|
|
156
|
+
if (!prerendered.includes('/')) {
|
|
157
|
+
const file = join(ctx.clientDir, '_redirects');
|
|
158
|
+
const existing = await readFile(file, 'utf-8').catch(() => '');
|
|
159
|
+
const rule = '/ /.netlify/functions/fluixi-server 200!\n';
|
|
160
|
+
if (!existing.includes(rule)) {
|
|
161
|
+
await writeFile(file, existing && !existing.endsWith('\n') ? `${existing}\n${rule}` : existing + rule);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Vercel, through the Build Output API v3: `.vercel/output/` is read directly, with
|
|
168
|
+
* no framework detection and no `vercel.json` to keep in sync.
|
|
169
|
+
*
|
|
170
|
+
* `config.json` routes the filesystem first so a prerendered page is served as a
|
|
171
|
+
* file, then sends the rest to the function.
|
|
172
|
+
*/
|
|
173
|
+
export const vercelAdapter = {
|
|
174
|
+
name: 'vercel',
|
|
175
|
+
bundle: 'inline',
|
|
176
|
+
serve({ handler }) {
|
|
177
|
+
return { fetch: handler };
|
|
178
|
+
},
|
|
179
|
+
async build(ctx) {
|
|
180
|
+
const output = join(ctx.root, '.vercel', 'output');
|
|
181
|
+
await mkdir(output, { recursive: true });
|
|
182
|
+
// Static assets and prerendered HTML are served straight from here.
|
|
183
|
+
await cp(ctx.clientDir, join(output, 'static'), { recursive: true });
|
|
184
|
+
if (skipWhenStatic(ctx, 'vercel')) {
|
|
185
|
+
await writeFile(join(output, 'config.json'), JSON.stringify({ version: 3, routes: [{ handle: 'filesystem' }] }, null, 2) + '\n');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const fn = join(output, 'functions', 'index.func');
|
|
189
|
+
await mkdir(fn, { recursive: true });
|
|
190
|
+
const out = join(fn, 'index.mjs');
|
|
191
|
+
const prerendered = await prerenderedRoutes(ctx.clientDir, ctx.cfg.mountId);
|
|
192
|
+
await ctx.bundleEntry(vercelEntry(await entrySource(ctx, out, prerendered)), out);
|
|
193
|
+
await writeFile(join(fn, '.vc-config.json'), JSON.stringify({ runtime: 'nodejs20.x', handler: 'index.mjs', launcherType: 'Nodejs', shouldAddHelpers: false }, null, 2) + '\n');
|
|
194
|
+
await writeFile(join(output, 'config.json'), JSON.stringify({
|
|
195
|
+
version: 3,
|
|
196
|
+
routes: [
|
|
197
|
+
// The filesystem holds a shell at `/index.html`, so `/` has to be claimed
|
|
198
|
+
// before that phase or it is served unrendered. Skipped when `/` really is
|
|
199
|
+
// a prerendered page, which the filesystem should answer.
|
|
200
|
+
...(prerendered.includes('/') ? [] : [{ src: '/', dest: '/index' }]),
|
|
201
|
+
// A prerendered page or an asset wins; only a miss reaches the function.
|
|
202
|
+
{ handle: 'filesystem' },
|
|
203
|
+
{ src: '/(.*)', dest: '/index' },
|
|
204
|
+
],
|
|
205
|
+
}, null, 2) + '\n');
|
|
206
|
+
},
|
|
207
|
+
};
|
package/dist/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,QAAQ;IACvB,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,6CAA6C;IAC7C,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7B,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;CACd;AAED,sFAAsF;AACtF,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,SAAS,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAqB5G;AAED,sFAAsF;AACtF,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,SAAS,GAAG,OAAO,CAEpE;AAED,0EAA0E;AAC1E,wBAAgB,cAAc,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,IAAI,CAS5I;AAED,gGAAgG;AAChG,wBAAsB,SAAS,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC,CAYxG;AAkBD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,GAAG,
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,QAAQ;IACvB,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,6CAA6C;IAC7C,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7B,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;CACd;AAED,sFAAsF;AACtF,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,SAAS,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAqB5G;AAED,sFAAsF;AACtF,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,SAAS,GAAG,OAAO,CAEpE;AAED,0EAA0E;AAC1E,wBAAgB,cAAc,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,IAAI,CAS5I;AAED,gGAAgG;AAChG,wBAAsB,SAAS,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC,CAYxG;AAkBD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,GAAG,CAuC/E;AAED,eAAO,MAAM,kBAAkB,uBAAW,CAAC"}
|
package/dist/api.js
CHANGED
|
@@ -90,11 +90,17 @@ export function apiRoutesVitePlugin(opts) {
|
|
|
90
90
|
const id = '\0' + API_VMOD;
|
|
91
91
|
return {
|
|
92
92
|
name: 'fluixi-api',
|
|
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 —
|
|
95
|
+
// the app builds and every API route 404s.
|
|
96
|
+
enforce: 'pre',
|
|
93
97
|
configResolved(config) {
|
|
94
98
|
root = config.root || root;
|
|
95
99
|
},
|
|
96
100
|
resolveId(source) {
|
|
97
|
-
|
|
101
|
+
// The public specifier is a real module so its types ship with the package; both
|
|
102
|
+
// resolve to the generated table.
|
|
103
|
+
return source === API_VMOD || source === '@fluixi/start/api-routes' ? id : null;
|
|
98
104
|
},
|
|
99
105
|
load(source) {
|
|
100
106
|
if (source !== id)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/commands/build.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/commands/build.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,cAAc,CAAC;AAOjE;;;;GAIG;AACH,wBAAsB,KAAK,CAAC,MAAM,GAAE,YAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqFpE"}
|
package/dist/commands/build.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import {
|
|
2
|
+
import { mkdir, writeFile, rm, copyFile } from 'node:fs/promises';
|
|
3
|
+
import { resolve, dirname, join, basename } from 'node:path';
|
|
3
4
|
import { resolveConfig } from '../config.js';
|
|
4
5
|
import { fluixiPlugins, loadServerEnv } from '../internal.js';
|
|
6
|
+
import { ssrBuildOptions } from '../adapter.js';
|
|
5
7
|
import { prerender } from './prerender.js';
|
|
6
8
|
import { wordmark, step, done, c } from '../ui.js';
|
|
7
9
|
/**
|
|
@@ -15,6 +17,8 @@ export async function build(config = {}) {
|
|
|
15
17
|
await loadServerEnv(cfg.root, 'production'); // .env → process.env (server-only secrets)
|
|
16
18
|
const { build: viteBuild } = await import('vite');
|
|
17
19
|
const plugins = await fluixiPlugins(cfg);
|
|
20
|
+
// The deploy target decides whether the framework is bundled in or imported at runtime.
|
|
21
|
+
const ssr = ssrBuildOptions(cfg);
|
|
18
22
|
process.stdout.write(`\n ${wordmark()} ${c.gray('building for production')}\n\n`);
|
|
19
23
|
step('client bundle');
|
|
20
24
|
await viteBuild({
|
|
@@ -27,11 +31,11 @@ export async function build(config = {}) {
|
|
|
27
31
|
// functions / middleware — so build it when the entry exists; a pure static SPA skips it.
|
|
28
32
|
const serverEntrySrc = resolve(cfg.root, cfg.serverEntry);
|
|
29
33
|
if (cfg.ssr || existsSync(serverEntrySrc)) {
|
|
30
|
-
step(
|
|
34
|
+
step(`server bundle${bundleNote(cfg)}`);
|
|
31
35
|
await viteBuild({
|
|
32
36
|
root: cfg.root,
|
|
33
37
|
plugins,
|
|
34
|
-
ssr
|
|
38
|
+
ssr,
|
|
35
39
|
build: {
|
|
36
40
|
outDir: 'dist/server',
|
|
37
41
|
ssr: serverEntrySrc,
|
|
@@ -48,7 +52,7 @@ export async function build(config = {}) {
|
|
|
48
52
|
root: cfg.root,
|
|
49
53
|
plugins,
|
|
50
54
|
logLevel: 'warn',
|
|
51
|
-
ssr
|
|
55
|
+
ssr,
|
|
52
56
|
build: {
|
|
53
57
|
outDir: 'dist/server',
|
|
54
58
|
ssr: middlewareSrc,
|
|
@@ -58,10 +62,75 @@ export async function build(config = {}) {
|
|
|
58
62
|
}
|
|
59
63
|
// SSG: prerender routes to static HTML in dist/client (config-driven).
|
|
60
64
|
if (cfg.prerender) {
|
|
65
|
+
// Prerendering `/` writes over the built `index.html`, and that file is also the
|
|
66
|
+
// template the server renders into. Keep the shell before it goes: without this a
|
|
67
|
+
// prerendered home page becomes the template, so every SSR response carries the
|
|
68
|
+
// home page's markup and head around the app it just rendered.
|
|
69
|
+
await copyFile(resolve(cfg.root, 'dist/client', 'index.html'), resolve(cfg.root, 'dist/server', 'template.html'));
|
|
61
70
|
process.stdout.write('\n');
|
|
62
71
|
await prerender(config);
|
|
63
72
|
}
|
|
73
|
+
// The deploy target's own output layout — a worker entry, a function manifest, a
|
|
74
|
+
// routing config. Last, so everything it wraps or copies already exists.
|
|
75
|
+
if (cfg.adapter?.build) {
|
|
76
|
+
step(`${cfg.adapter.name} output`);
|
|
77
|
+
await cfg.adapter.build({
|
|
78
|
+
cfg,
|
|
79
|
+
root: cfg.root,
|
|
80
|
+
clientDir: resolve(cfg.root, 'dist/client'),
|
|
81
|
+
serverDir: resolve(cfg.root, 'dist/server'),
|
|
82
|
+
hasServer: existsSync(resolve(cfg.root, 'dist/server', 'entry-server.js')),
|
|
83
|
+
bundleEntry: (source, outFile) => bundleEntry(cfg, plugins, source, outFile),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
64
86
|
const ms = Date.now() - startedAt;
|
|
65
87
|
process.stdout.write('\n');
|
|
66
88
|
done(`${c.bold('built')} in ${c.bold(String(ms))} ms ${c.gray('→ dist/client + dist/server')}\n`);
|
|
67
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Bundle a generated platform entry into one self-contained file.
|
|
92
|
+
*
|
|
93
|
+
* The entry is written next to its output so a relative import of the server bundle
|
|
94
|
+
* resolves, then removed — a stray `.fluixi-entry.js` in dist would be served as a
|
|
95
|
+
* static asset on every one of these platforms.
|
|
96
|
+
*/
|
|
97
|
+
async function bundleEntry(cfg, plugins, source, outFile) {
|
|
98
|
+
const { build: viteBuild } = await import('vite');
|
|
99
|
+
const dir = dirname(outFile);
|
|
100
|
+
const temp = join(dir, `.fluixi-entry-${Date.now()}.js`);
|
|
101
|
+
await mkdir(dir, { recursive: true });
|
|
102
|
+
await writeFile(temp, source);
|
|
103
|
+
try {
|
|
104
|
+
await viteBuild({
|
|
105
|
+
root: cfg.root,
|
|
106
|
+
plugins: plugins,
|
|
107
|
+
logLevel: 'warn',
|
|
108
|
+
// Nothing may be left to resolve: these run as an uploaded file, not an install.
|
|
109
|
+
ssr: { noExternal: true },
|
|
110
|
+
build: {
|
|
111
|
+
outDir: dir,
|
|
112
|
+
emptyOutDir: false,
|
|
113
|
+
ssr: temp,
|
|
114
|
+
rollupOptions: {
|
|
115
|
+
output: {
|
|
116
|
+
entryFileNames: basename(outFile),
|
|
117
|
+
// One file, no chunks. A lazy route would otherwise be split out beside
|
|
118
|
+
// the entry — and for Cloudflare that directory is the published one, so
|
|
119
|
+
// every server chunk would be a public download.
|
|
120
|
+
inlineDynamicImports: true,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
await rm(temp, { force: true });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** `server bundle (node · external)` — the output shape is not obvious from the file. */
|
|
131
|
+
function bundleNote(cfg) {
|
|
132
|
+
const adapter = cfg.adapter;
|
|
133
|
+
if (!adapter?.bundle)
|
|
134
|
+
return '';
|
|
135
|
+
return c.gray(` (${adapter.name} · ${adapter.bundle})`);
|
|
136
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* Kept off the package root deliberately: these reach the dev server, the build
|
|
5
|
+
* and the prerenderer, and a module that only wants `defineMiddleware` should not
|
|
6
|
+
* be able to pull a bundler in behind it. That matters at build time — a deploy
|
|
7
|
+
* adapter inlines everything the entry can reach, and reaching this would put
|
|
8
|
+
* vite in a worker.
|
|
9
|
+
*/
|
|
10
|
+
export { dev } from './dev.js';
|
|
11
|
+
export { build } from './build.js';
|
|
12
|
+
export { start } from './start.js';
|
|
13
|
+
export { prerender } from './prerender.js';
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/commands/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAC/B,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* Kept off the package root deliberately: these reach the dev server, the build
|
|
5
|
+
* and the prerenderer, and a module that only wants `defineMiddleware` should not
|
|
6
|
+
* be able to pull a bundler in behind it. That matters at build time — a deploy
|
|
7
|
+
* adapter inlines everything the entry can reach, and reaching this would put
|
|
8
|
+
* vite in a worker.
|
|
9
|
+
*/
|
|
10
|
+
export { dev } from './dev.js';
|
|
11
|
+
export { build } from './build.js';
|
|
12
|
+
export { start } from './start.js';
|
|
13
|
+
export { prerender } from './prerender.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start.d.ts","sourceRoot":"","sources":["../../src/commands/start.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAKjD;;;;;GAKG;AACH,wBAAsB,KAAK,CAAC,MAAM,GAAE,YAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"start.d.ts","sourceRoot":"","sources":["../../src/commands/start.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAKjD;;;;;GAKG;AACH,wBAAsB,KAAK,CAAC,MAAM,GAAE,YAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBpE"}
|
package/dist/commands/start.js
CHANGED
|
@@ -14,5 +14,14 @@ export async function start(config = {}) {
|
|
|
14
14
|
await loadServerEnv(cfg.root, 'production'); // .env → process.env (server-only secrets)
|
|
15
15
|
const clientDir = resolve(cfg.root, 'dist/client');
|
|
16
16
|
const handler = await createProdHandler(cfg);
|
|
17
|
-
|
|
17
|
+
// The configured adapter, or node when there is none. An adapter that returns
|
|
18
|
+
// `{ fetch }` has no process to run — its output is a module the platform imports —
|
|
19
|
+
// so say that instead of exiting as if the server had started.
|
|
20
|
+
const adapter = cfg.adapter ?? nodeAdapter;
|
|
21
|
+
const served = await adapter.serve({ handler, cfg, clientDir });
|
|
22
|
+
if (served && typeof served === 'object' && 'fetch' in served) {
|
|
23
|
+
console.log(` fluixi: the "${adapter.name}" adapter exports a fetch handler — there is no server ` +
|
|
24
|
+
`to start. Deploy dist/ and let the platform invoke it, or run \`fluixi start\` with ` +
|
|
25
|
+
`a filesystem adapter (nodeAdapter).`);
|
|
26
|
+
}
|
|
18
27
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Adapter } from './adapter.js';
|
|
1
2
|
/**
|
|
2
3
|
* Fluixi app config. Place in `fluixi.config.{ts,js,mjs}` at the app root and wrap
|
|
3
4
|
* with `defineConfig` for types. Everything is optional — the defaults match the
|
|
@@ -31,6 +32,12 @@ export interface FluixiConfig {
|
|
|
31
32
|
middleware?: string;
|
|
32
33
|
/** SSR deps Vite must bundle (no-external) — e.g. ESM packages without file extensions. */
|
|
33
34
|
ssrNoExternal?: string[];
|
|
35
|
+
/**
|
|
36
|
+
* Deploy target. Decides how `fluixi build` bundles the server (see the adapter's
|
|
37
|
+
* `bundle` mode) and how `fluixi start` serves it. Unset keeps Vite's own bundling
|
|
38
|
+
* heuristic and serves with `nodeAdapter`.
|
|
39
|
+
*/
|
|
40
|
+
adapter?: Adapter;
|
|
34
41
|
/**
|
|
35
42
|
* Built-in i18n auto-discovery. Scans `<dir>/<locale>.json` and exposes them as
|
|
36
43
|
* `virtual:fluixi-i18n` (`{ messages, locales, defaultLocale }`) to feed `createI18n`.
|
|
@@ -64,10 +71,12 @@ export interface ResolvedPrerender {
|
|
|
64
71
|
routes: string[];
|
|
65
72
|
crawl: boolean;
|
|
66
73
|
}
|
|
67
|
-
export type ResolvedConfig = Required<Omit<FluixiConfig, 'ssrNoExternal' | 'i18n' | 'prerender'>> & {
|
|
74
|
+
export type ResolvedConfig = Required<Omit<FluixiConfig, 'ssrNoExternal' | 'i18n' | 'prerender' | 'adapter'>> & {
|
|
68
75
|
ssrNoExternal: string[];
|
|
69
76
|
i18n: ResolvedI18n | false;
|
|
70
77
|
prerender: ResolvedPrerender | false;
|
|
78
|
+
/** Stays optional: no adapter means "Vite's defaults + serve with node". */
|
|
79
|
+
adapter?: Adapter;
|
|
71
80
|
};
|
|
72
81
|
/** Identity helper for editor types in `fluixi.config.ts`. */
|
|
73
82
|
export declare function defineConfig(config: FluixiConfig): FluixiConfig;
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,kDAAkD;IAClD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mGAAmG;IACnG,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qFAAqF;IACrF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6GAA6G;IAC7G,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wGAAwG;IACxG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2FAA2F;IAC3F,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,OAAO,GAAG;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAC9E;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAC9D;AAED,iEAAiE;AACjE,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,sDAAsD;AACtD,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,MAAM,cAAc,GAAG,QAAQ,CACnC,IAAI,CAAC,YAAY,EAAE,eAAe,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,CAAC,CACvE,GAAG;IACF,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,IAAI,EAAE,YAAY,GAAG,KAAK,CAAC;IAC3B,SAAS,EAAE,iBAAiB,GAAG,KAAK,CAAC;IACrC,4EAA4E;IAC5E,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,8DAA8D;AAC9D,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CAE/D;AAED,sBAAsB;AACtB,wBAAgB,aAAa,CAAC,MAAM,GAAE,YAAiB,GAAG,cAAc,CAkBvE;AAgCD;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,IAAI,SAAgB,GAAG,OAAO,CAAC,YAAY,CAAC,CAM5E"}
|
package/dist/config.js
CHANGED
|
@@ -21,6 +21,7 @@ export function resolveConfig(config = {}) {
|
|
|
21
21
|
apiDir: config.apiDir ?? 'src/api',
|
|
22
22
|
middleware: config.middleware ?? 'src/middleware.ts',
|
|
23
23
|
ssrNoExternal: config.ssrNoExternal ?? [],
|
|
24
|
+
adapter: config.adapter,
|
|
24
25
|
i18n: resolveI18n(config.i18n),
|
|
25
26
|
prerender: resolvePrerender(config.prerender),
|
|
26
27
|
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { FetchHandler } from './handler.js';
|
|
2
|
+
/**
|
|
3
|
+
* Inject server-rendered app HTML into the template's mount element
|
|
4
|
+
* (`<div id="root"></div>`). Falls back to a `<!--ssr-outlet-->` marker, then to
|
|
5
|
+
* just before `</body>`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function injectApp(template: string, appHtml: string, mountId?: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Split the template at the mount point for streaming: `head` is everything up to and
|
|
10
|
+
* including the open mount tag (flushed first so the browser fetches assets while the
|
|
11
|
+
* server awaits data); `tail` is the close tag onward (client script + `</body>`).
|
|
12
|
+
* Mirrors injectApp's three cases (mount div, `<!--ssr-outlet-->`, before `</body>`).
|
|
13
|
+
*/
|
|
14
|
+
export declare function splitTemplate(template: string, mountId?: string): {
|
|
15
|
+
head: string;
|
|
16
|
+
tail: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Inject the rendered app into the mount point AND lift any @fluixi/head metadata (carried back as
|
|
20
|
+
* a marker by renderRequestAsync) into <head> + <html>. If the page set a title, the template's own
|
|
21
|
+
* <title> is dropped so there's only one. No marker → identical to injectApp.
|
|
22
|
+
*/
|
|
23
|
+
export declare function injectAppAndHead(template: string, rendered: string, mountId?: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Wrap a fetch handler so a thrown/rejected error becomes a 500 instead of an
|
|
26
|
+
* unhandled rejection. `createRequestHandler` already guards the renderer; this
|
|
27
|
+
* covers the middleware chain that wraps it. `onError` lets dev map the stack first.
|
|
28
|
+
*/
|
|
29
|
+
export declare function guardHandler(handler: FetchHandler, onError?: (e: unknown) => void): FetchHandler;
|
|
30
|
+
//# sourceMappingURL=document.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,SAAS,GAAG,MAAM,CASrF;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,SAAS,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAwBhG;AASD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,SAAS,GAAG,MAAM,CAW7F;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,YAAY,EACrB,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,GAC7B,YAAY,CAYd"}
|
package/dist/document.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a rendered app into a document, and keeping a failed render from taking
|
|
3
|
+
* the process with it.
|
|
4
|
+
*
|
|
5
|
+
* A leaf on purpose: this is imported by the request handler, which runs in a
|
|
6
|
+
* worker as often as on Node, and a runtime module that reaches into the build
|
|
7
|
+
* helpers drags a whole toolchain into the bundle with it.
|
|
8
|
+
*/
|
|
9
|
+
import { extractHeadMarker } from '@fluixi/head';
|
|
10
|
+
/**
|
|
11
|
+
* Inject server-rendered app HTML into the template's mount element
|
|
12
|
+
* (`<div id="root"></div>`). Falls back to a `<!--ssr-outlet-->` marker, then to
|
|
13
|
+
* just before `</body>`.
|
|
14
|
+
*/
|
|
15
|
+
export function injectApp(template, appHtml, mountId = 'root') {
|
|
16
|
+
const mount = new RegExp(`<div id=["']${mountId}["']\\s*>\\s*</div>`);
|
|
17
|
+
if (mount.test(template)) {
|
|
18
|
+
return template.replace(mount, `<div id="${mountId}">${appHtml}</div>`);
|
|
19
|
+
}
|
|
20
|
+
if (template.includes('<!--ssr-outlet-->')) {
|
|
21
|
+
return template.replace('<!--ssr-outlet-->', appHtml);
|
|
22
|
+
}
|
|
23
|
+
return template.replace('</body>', `<div id="${mountId}">${appHtml}</div></body>`);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Split the template at the mount point for streaming: `head` is everything up to and
|
|
27
|
+
* including the open mount tag (flushed first so the browser fetches assets while the
|
|
28
|
+
* server awaits data); `tail` is the close tag onward (client script + `</body>`).
|
|
29
|
+
* Mirrors injectApp's three cases (mount div, `<!--ssr-outlet-->`, before `</body>`).
|
|
30
|
+
*/
|
|
31
|
+
export function splitTemplate(template, mountId = 'root') {
|
|
32
|
+
const mount = new RegExp(`<div id=["']${mountId}["']\\s*>\\s*</div>`);
|
|
33
|
+
const m = template.match(mount);
|
|
34
|
+
if (m && m.index !== undefined) {
|
|
35
|
+
return {
|
|
36
|
+
head: template.slice(0, m.index) + `<div id="${mountId}">`,
|
|
37
|
+
tail: `</div>` + template.slice(m.index + m[0].length),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const outlet = template.indexOf('<!--ssr-outlet-->');
|
|
41
|
+
if (outlet !== -1) {
|
|
42
|
+
return {
|
|
43
|
+
head: template.slice(0, outlet),
|
|
44
|
+
tail: template.slice(outlet + '<!--ssr-outlet-->'.length),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const body = template.indexOf('</body>');
|
|
48
|
+
if (body !== -1) {
|
|
49
|
+
return {
|
|
50
|
+
head: template.slice(0, body) + `<div id="${mountId}">`,
|
|
51
|
+
tail: `</div>` + template.slice(body),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return { head: template, tail: '' };
|
|
55
|
+
}
|
|
56
|
+
/** Set/replace an attribute on the template's <html> tag (e.g. lang). */
|
|
57
|
+
function setHtmlAttr(html, name, value) {
|
|
58
|
+
const existing = new RegExp(`(<html\\b[^>]*?)\\s${name}="[^"]*"`, 'i');
|
|
59
|
+
if (existing.test(html))
|
|
60
|
+
return html.replace(existing, `$1 ${name}="${value}"`);
|
|
61
|
+
return html.replace(/<html\b/i, `<html ${name}="${value}"`);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Inject the rendered app into the mount point AND lift any @fluixi/head metadata (carried back as
|
|
65
|
+
* a marker by renderRequestAsync) into <head> + <html>. If the page set a title, the template's own
|
|
66
|
+
* <title> is dropped so there's only one. No marker → identical to injectApp.
|
|
67
|
+
*/
|
|
68
|
+
export function injectAppAndHead(template, rendered, mountId = 'root') {
|
|
69
|
+
const { head, body } = extractHeadMarker(rendered);
|
|
70
|
+
let html = injectApp(template, body, mountId);
|
|
71
|
+
if (head) {
|
|
72
|
+
if (head.headHtml) {
|
|
73
|
+
if (/<title[\s>]/i.test(head.headHtml))
|
|
74
|
+
html = html.replace(/<title>[\s\S]*?<\/title>/i, '');
|
|
75
|
+
html = html.replace('</head>', `${head.headHtml}</head>`);
|
|
76
|
+
}
|
|
77
|
+
for (const [k, v] of Object.entries(head.htmlAttrs))
|
|
78
|
+
html = setHtmlAttr(html, k, v);
|
|
79
|
+
}
|
|
80
|
+
return html;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Wrap a fetch handler so a thrown/rejected error becomes a 500 instead of an
|
|
84
|
+
* unhandled rejection. `createRequestHandler` already guards the renderer; this
|
|
85
|
+
* covers the middleware chain that wraps it. `onError` lets dev map the stack first.
|
|
86
|
+
*/
|
|
87
|
+
export function guardHandler(handler, onError) {
|
|
88
|
+
return async (request) => {
|
|
89
|
+
try {
|
|
90
|
+
return await handler(request);
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
onError?.(e);
|
|
94
|
+
return new Response(String(e?.stack || e), {
|
|
95
|
+
status: 500,
|
|
96
|
+
headers: { 'content-type': 'text/plain; charset=utf-8' },
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Whether this request addresses a file-based API route. */
|
|
2
|
+
export declare function isApiRequest(_request: Request): boolean;
|
|
3
|
+
/** Run the matched API handler and return its response. */
|
|
4
|
+
export declare function handleApiRequest(_request: Request): Promise<Response>;
|
|
5
|
+
//# sourceMappingURL=generated-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generated-api.d.ts","sourceRoot":"","sources":["../src/generated-api.ts"],"names":[],"mappings":"AAqBA,6DAA6D;AAC7D,wBAAgB,YAAY,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAEvD;AAED,2DAA2D;AAC3D,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAErE"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The file-based API routes found under `apiDir` (default `src/api`).
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* import { isApiRequest, handleApiRequest } from '@fluixi/start/api-routes';
|
|
6
|
+
* ```
|
|
7
|
+
*
|
|
8
|
+
* Named `api-routes` because `@fluixi/start/api` is already the API plumbing you call
|
|
9
|
+
* directly (`createApiHandler`, `imageLoader`, …); this is the generated route table.
|
|
10
|
+
*
|
|
11
|
+
* A typed stand-in — the scan plugin serves the real one. `virtual:fluixi-api` still
|
|
12
|
+
* resolves.
|
|
13
|
+
*/
|
|
14
|
+
function missingPlugin() {
|
|
15
|
+
throw new Error("[fluixi] '@fluixi/start/api-routes' was imported but the API scan plugin did not replace " +
|
|
16
|
+
'it. Build with `fluixi build`/`fluixi dev`, or add the @fluixi/start plugins to your ' +
|
|
17
|
+
'Vite config.');
|
|
18
|
+
}
|
|
19
|
+
/** Whether this request addresses a file-based API route. */
|
|
20
|
+
export function isApiRequest(_request) {
|
|
21
|
+
missingPlugin();
|
|
22
|
+
}
|
|
23
|
+
/** Run the matched API handler and return its response. */
|
|
24
|
+
export function handleApiRequest(_request) {
|
|
25
|
+
missingPlugin();
|
|
26
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Whether this request is an RPC call to a `"use server"` function. */
|
|
2
|
+
export declare function isServerFnRequest(_request: Request): boolean;
|
|
3
|
+
/** Run the addressed server function and return its response. */
|
|
4
|
+
export declare function handleServerFn(_request: Request): Promise<Response>;
|
|
5
|
+
//# sourceMappingURL=generated-server-fns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generated-server-fns.d.ts","sourceRoot":"","sources":["../src/generated-server-fns.ts"],"names":[],"mappings":"AAqBA,wEAAwE;AACxE,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAE5D;AAED,iEAAiE;AACjE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAEnE"}
|