@pracht/adapter-static 0.2.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +9 -3
- package/dist/index.mjs +73 -48
- package/package.json +9 -4
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { PrachtAdapter, PrachtAdapter as PrachtAdapter$1 } from "@pracht/core/adapter";
|
|
1
2
|
import { HeadMetadata } from "@pracht/core";
|
|
2
|
-
import { PrachtAdapter } from "@pracht/vite-plugin";
|
|
3
3
|
import { IncomingMessage, ServerResponse } from "node:http";
|
|
4
4
|
|
|
5
5
|
//#region src/static-entry.d.ts
|
|
@@ -41,7 +41,7 @@ declare function createStaticServerEntryModule(options?: StaticAdapterOptions):
|
|
|
41
41
|
* was requested. Configuring `fallback` requires full hydration there, because
|
|
42
42
|
* that document is built from the not-found page's serialized route state.
|
|
43
43
|
*/
|
|
44
|
-
declare function staticAdapter(options?: StaticAdapterOptions): PrachtAdapter;
|
|
44
|
+
declare function staticAdapter(options?: StaticAdapterOptions): PrachtAdapter$1;
|
|
45
45
|
//#endregion
|
|
46
46
|
//#region src/static-preview.d.ts
|
|
47
47
|
interface StaticPreviewHandlerOptions {
|
|
@@ -61,6 +61,12 @@ interface StaticPreviewHandlerOptions {
|
|
|
61
61
|
* export under a sub-path does.
|
|
62
62
|
*/
|
|
63
63
|
base?: string | null;
|
|
64
|
+
/**
|
|
65
|
+
* `pracht({ trailingSlash })`. Under `"always"` or `"never"` a page asked
|
|
66
|
+
* for by its other spelling is redirected (308, query kept), the way a host
|
|
67
|
+
* configured for that URL shape does. Files are served as named.
|
|
68
|
+
*/
|
|
69
|
+
trailingSlash?: "ignore" | "never" | "always" | null;
|
|
64
70
|
}
|
|
65
71
|
/**
|
|
66
72
|
* A deliberately tiny static file server for `pracht preview` of static
|
|
@@ -72,4 +78,4 @@ interface StaticPreviewHandlerOptions {
|
|
|
72
78
|
*/
|
|
73
79
|
declare function createStaticPreviewHandler(options: StaticPreviewHandlerOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
74
80
|
//#endregion
|
|
75
|
-
export { type StaticAdapterOptions, type StaticPreviewHandlerOptions, createStaticPreviewHandler, createStaticServerEntryModule, staticAdapter };
|
|
81
|
+
export { type PrachtAdapter, type StaticAdapterOptions, type StaticPreviewHandlerOptions, createStaticPreviewHandler, createStaticServerEntryModule, staticAdapter };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { resolveStaticFile } from "@pracht/adapter-node";
|
|
4
|
+
import { decodeStaticPath, staticByteRange, staticPathCandidates } from "@pracht/core/server";
|
|
4
5
|
//#region src/static-entry.ts
|
|
5
6
|
const FALLBACK_FILENAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*\.html$/;
|
|
6
7
|
const WINDOWS_RESERVED_FALLBACK_NAME_RE = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i;
|
|
@@ -105,6 +106,7 @@ function createStaticServerEntryModule(options = {}) {
|
|
|
105
106
|
" staticDir: resolve(serverDir, \"../client\"),",
|
|
106
107
|
" fallback: staticExportConfig.fallback,",
|
|
107
108
|
" base: buildBase,",
|
|
109
|
+
" trailingSlash: buildTrailingSlash,",
|
|
108
110
|
" });",
|
|
109
111
|
" const server = createServer(handler);",
|
|
110
112
|
` const port = Number(process.env.PORT ?? ${port});`,
|
|
@@ -115,25 +117,6 @@ function createStaticServerEntryModule(options = {}) {
|
|
|
115
117
|
""
|
|
116
118
|
].join("\n");
|
|
117
119
|
}
|
|
118
|
-
/**
|
|
119
|
-
* Create a pracht adapter for pure static export.
|
|
120
|
-
*
|
|
121
|
-
* ```ts
|
|
122
|
-
* import { staticAdapter } from "@pracht/adapter-static";
|
|
123
|
-
* pracht({ adapter: staticAdapter() })
|
|
124
|
-
* ```
|
|
125
|
-
*
|
|
126
|
-
* `pracht build` prerenders every route into `dist/client/` — deploy that
|
|
127
|
-
* directory to any static host. Build-time validation fails closed on
|
|
128
|
-
* anything that needs a server: `render: "ssr"` / `"isg"` routes, SPA
|
|
129
|
-
* loaders, request middleware, API routes, and network-exposed capabilities.
|
|
130
|
-
* SSG loaders run at build time; static SPA routes are loaderless.
|
|
131
|
-
*
|
|
132
|
-
* The `notFound` page may use `hydration: "islands"` or `"none"` to keep the
|
|
133
|
-
* client router out of `404.html`, at the cost of the page knowing which URL
|
|
134
|
-
* was requested. Configuring `fallback` requires full hydration there, because
|
|
135
|
-
* that document is built from the not-found page's serialized route state.
|
|
136
|
-
*/
|
|
137
120
|
function staticAdapter(options = {}) {
|
|
138
121
|
assertValidStaticAdapterOptions(options);
|
|
139
122
|
return {
|
|
@@ -153,19 +136,14 @@ function staticAdapter(options = {}) {
|
|
|
153
136
|
}
|
|
154
137
|
//#endregion
|
|
155
138
|
//#region src/static-preview.ts
|
|
156
|
-
/**
|
|
157
|
-
* A deliberately tiny static file server for `pracht preview` of static
|
|
158
|
-
* exports. It mirrors what a plain static host does — files, clean-URL
|
|
159
|
-
* `index.html` resolution, `404.html` for misses, an optional SPA fallback —
|
|
160
|
-
* and nothing else: no route matching, no loaders, no framework runtime.
|
|
161
|
-
* Production deploys should use a real static host; this exists so the build
|
|
162
|
-
* output can be exercised locally with one command.
|
|
163
|
-
*/
|
|
164
139
|
function createStaticPreviewHandler(options) {
|
|
165
140
|
const staticDir = resolve(options.staticDir);
|
|
166
141
|
const fallback = options.fallback ?? null;
|
|
167
142
|
const base = normalizePreviewBase(options.base);
|
|
168
|
-
const
|
|
143
|
+
const trailingSlash = options.trailingSlash ?? "ignore";
|
|
144
|
+
const decodedBase = decodePathSegments(new URL(base, "http://localhost").pathname);
|
|
145
|
+
if (!decodedBase) throw new Error(`Unsafe static preview base ${JSON.stringify(base)}.`);
|
|
146
|
+
const baseSegments = decodedBase.slice(1, -1);
|
|
169
147
|
return async function handleStaticPreviewRequest(req, res) {
|
|
170
148
|
const method = req.method ?? "GET";
|
|
171
149
|
if (method !== "GET" && method !== "HEAD") {
|
|
@@ -178,31 +156,48 @@ function createStaticPreviewHandler(options) {
|
|
|
178
156
|
}
|
|
179
157
|
let pathname;
|
|
180
158
|
let search;
|
|
159
|
+
let publicPathname;
|
|
181
160
|
try {
|
|
182
161
|
const requestUrl = new URL(req.url ?? "/", "http://localhost");
|
|
183
|
-
|
|
162
|
+
publicPathname = requestUrl.pathname;
|
|
163
|
+
pathname = requestUrl.pathname;
|
|
184
164
|
search = requestUrl.search;
|
|
185
165
|
} catch {
|
|
166
|
+
pathname = "";
|
|
167
|
+
search = "";
|
|
168
|
+
publicPathname = "";
|
|
169
|
+
}
|
|
170
|
+
if (!decodeStaticPath(pathname).ok || staticPathCandidates(pathname) === null) {
|
|
186
171
|
res.writeHead(400, { "content-type": "text/plain; charset=utf-8" });
|
|
187
172
|
res.end("Bad request");
|
|
188
173
|
return;
|
|
189
174
|
}
|
|
190
|
-
if (
|
|
191
|
-
|
|
175
|
+
if (baseSegments.length > 0) {
|
|
176
|
+
const segments = pathname.split("/").slice(1);
|
|
177
|
+
const decodedSegments = decodePathSegments(pathname)?.slice(1) ?? segments;
|
|
178
|
+
if (decodedSegments.length === baseSegments.length && baseSegments.every((segment, index) => decodedSegments[index] === segment)) {
|
|
192
179
|
res.writeHead(301, { location: `${base}${search}` });
|
|
193
180
|
res.end();
|
|
194
181
|
return;
|
|
195
182
|
}
|
|
196
|
-
if (!
|
|
183
|
+
if (decodedSegments.length <= baseSegments.length || !baseSegments.every((segment, index) => decodedSegments[index] === segment)) {
|
|
197
184
|
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
198
185
|
res.end("Not found");
|
|
199
186
|
return;
|
|
200
187
|
}
|
|
201
|
-
pathname = `/${
|
|
188
|
+
pathname = `/${segments.slice(baseSegments.length).join("/")}`;
|
|
202
189
|
}
|
|
203
190
|
const file = await resolveStaticFile(staticDir, pathname);
|
|
191
|
+
if (file?.document && trailingSlash !== "ignore") {
|
|
192
|
+
const canonical = canonicalPagePathname(publicPathname, base, trailingSlash);
|
|
193
|
+
if (canonical !== publicPathname) {
|
|
194
|
+
res.writeHead(308, { location: `${canonical.replace(/^\/{2,}/, "/")}${search}` });
|
|
195
|
+
res.end();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
204
199
|
if (file) {
|
|
205
|
-
await sendFile(res, method, file.filePath, file.contentType, file.cacheControl, 200);
|
|
200
|
+
await sendFile(res, method, file.filePath, file.contentType, file.cacheControl, 200, req);
|
|
206
201
|
return;
|
|
207
202
|
}
|
|
208
203
|
if (fallback) {
|
|
@@ -221,18 +216,22 @@ function createStaticPreviewHandler(options) {
|
|
|
221
216
|
res.end("Not found");
|
|
222
217
|
};
|
|
223
218
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
219
|
+
function canonicalPagePathname(pathname, base, policy) {
|
|
220
|
+
const trimmed = pathname.replace(/\/+$/, "");
|
|
221
|
+
if (trimmed === "" || `${trimmed}/` === base) return pathname;
|
|
222
|
+
if (policy === "never") return trimmed;
|
|
223
|
+
return trimmed.slice(trimmed.lastIndexOf("/") + 1).includes(".") ? trimmed : `${trimmed}/`;
|
|
224
|
+
}
|
|
225
|
+
function decodePathSegments(pathname) {
|
|
226
|
+
const segments = [];
|
|
227
|
+
for (const segment of pathname.split("/")) {
|
|
228
|
+
const decoded = decodeStaticPath(segment);
|
|
229
|
+
if (!decoded.ok) return null;
|
|
230
|
+
segments.push(decoded.path);
|
|
231
|
+
}
|
|
232
|
+
return segments;
|
|
234
233
|
}
|
|
235
|
-
async function sendFile(res, method, filePath, contentType, cacheControl, status) {
|
|
234
|
+
async function sendFile(res, method, filePath, contentType, cacheControl, status, rangeRequest) {
|
|
236
235
|
let body;
|
|
237
236
|
try {
|
|
238
237
|
body = await readFile(filePath);
|
|
@@ -241,14 +240,40 @@ async function sendFile(res, method, filePath, contentType, cacheControl, status
|
|
|
241
240
|
res.end("Not found");
|
|
242
241
|
return;
|
|
243
242
|
}
|
|
244
|
-
|
|
243
|
+
const headers = {
|
|
245
244
|
"content-type": contentType,
|
|
246
245
|
"cache-control": cacheControl,
|
|
247
246
|
"content-length": body.byteLength
|
|
248
|
-
}
|
|
247
|
+
};
|
|
248
|
+
if (rangeRequest) {
|
|
249
|
+
headers["accept-ranges"] = "bytes";
|
|
250
|
+
const range = method === "GET" ? staticByteRange({ get: (name) => headerValue(rangeRequest, name) }, { get: () => null }, body.byteLength) : null;
|
|
251
|
+
if (range === "unsatisfiable") {
|
|
252
|
+
res.writeHead(416, {
|
|
253
|
+
...headers,
|
|
254
|
+
"content-length": 0,
|
|
255
|
+
"content-range": `bytes */${body.byteLength}`
|
|
256
|
+
});
|
|
257
|
+
res.end();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (range) {
|
|
261
|
+
res.writeHead(206, {
|
|
262
|
+
...headers,
|
|
263
|
+
"content-length": range.end - range.start + 1,
|
|
264
|
+
"content-range": `bytes ${range.start}-${range.end}/${body.byteLength}`
|
|
265
|
+
});
|
|
266
|
+
res.end(body.subarray(range.start, range.end + 1));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
res.writeHead(status, headers);
|
|
249
271
|
res.end(method === "HEAD" ? void 0 : body);
|
|
250
272
|
}
|
|
251
|
-
|
|
273
|
+
function headerValue(req, name) {
|
|
274
|
+
const value = req.headers[name.toLowerCase()];
|
|
275
|
+
return typeof value === "string" ? value : null;
|
|
276
|
+
}
|
|
252
277
|
function normalizePreviewBase(raw) {
|
|
253
278
|
if (typeof raw !== "string" || raw === "" || raw === "/") return "/";
|
|
254
279
|
const withLeadingSlash = raw.startsWith("/") ? raw : `/${raw}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pracht/adapter-static",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Static-export adapter for Pracht: prerender every route and deploy dist/client to any static host with zero server.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pracht",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"vite"
|
|
14
14
|
],
|
|
15
15
|
"license": "MIT",
|
|
16
|
-
"homepage": "https://
|
|
16
|
+
"homepage": "https://pracht.resynapse.dev/docs/adapters",
|
|
17
17
|
"bugs": {
|
|
18
18
|
"url": "https://github.com/JoviDeCroock/pracht/issues"
|
|
19
19
|
},
|
|
@@ -39,8 +39,13 @@
|
|
|
39
39
|
"provenance": true
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@pracht/adapter-node": "^0.
|
|
43
|
-
|
|
42
|
+
"@pracht/adapter-node": "^1.0.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@pracht/core": "^1.0.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@pracht/core": "^1.0.0"
|
|
44
49
|
},
|
|
45
50
|
"scripts": {
|
|
46
51
|
"build": "tsdown"
|