@pracht/adapter-static 0.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/LICENSE +21 -0
- package/README.md +86 -0
- package/dist/index.d.mts +63 -0
- package/dist/index.mjs +212 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jovi De Croock
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# @pracht/adapter-static
|
|
2
|
+
|
|
3
|
+
Pure static export for pracht — the `adapter-static` / `output: "export"` of the framework. `pracht build` prerenders every route into `dist/client/`, and that directory deploys to any static host (GitHub Pages, S3, nginx, Netlify) with zero server.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @pracht/adapter-static
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// vite.config.ts
|
|
15
|
+
import { pracht } from "@pracht/vite-plugin";
|
|
16
|
+
import { staticAdapter } from "@pracht/adapter-static";
|
|
17
|
+
|
|
18
|
+
export default {
|
|
19
|
+
plugins: [pracht({ adapter: staticAdapter() })],
|
|
20
|
+
};
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pracht build # writes the deployable site to dist/client/
|
|
27
|
+
pracht preview # serves dist/client/ locally with a tiny static file server
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## What must hold
|
|
31
|
+
|
|
32
|
+
A static export has no server, so the build fails closed on anything that needs one:
|
|
33
|
+
|
|
34
|
+
- Every route must be `render: "ssg"` or loaderless, full-hydration `"spa"`. SSG loaders run at build time and must produce HTML plus valid JSON route state; loader redirects/errors, successful non-HTML responses, malformed route-state responses, and dynamic SSG routes without `getStaticPaths()` fail the build. Render failures report the underlying error, which the rendered 500 response itself withholds. SPA loaders, non-full SPA hydration, `ssr`, and `isg` are build errors naming the routes; use browser-side fetching for live SPA data or use `@pracht/adapter-node`, `@pracht/adapter-cloudflare`, or `@pracht/adapter-vercel`.
|
|
35
|
+
- Route and not-found middleware are build errors because no request runtime exists to enforce them.
|
|
36
|
+
- The `notFound` page must use full hydration (the default). A static host serves the same prebuilt `404.html` for every miss, so the full client router is required to adopt the visitor's actual URL.
|
|
37
|
+
- API routes are build errors.
|
|
38
|
+
- Manifest-registered capabilities exposed over HTTP/MCP/WebMCP are build errors (server-only capabilities invoked from build-time loaders are fine). Registered capability modules must load successfully so validation can fail closed; unused files in the capabilities directory are ignored.
|
|
39
|
+
- Route patterns and concrete paths returned by `getStaticPaths()` may not write under the reserved `/_pracht/` namespace. Concrete output is preflighted before any page is written.
|
|
40
|
+
- A Vite `base` other than `/` is a build error: prerendered documents reference `/assets/…` and `/_pracht/state/…` from the origin root, so a sub-path deploy (GitHub Pages project site, S3 key prefix) would build cleanly and serve a site whose every asset 404s.
|
|
41
|
+
|
|
42
|
+
The build also warns — without failing — on a `fallback` document in an app with no `notFound` page and no unshadowed client-routable SPA catch-all (unknown URLs would render blank).
|
|
43
|
+
|
|
44
|
+
Non-ASCII `getStaticPaths()` params are written to their decoded output path (`/posts/caf%C3%A9` → `posts/café/index.html`), matching how static hosts resolve requests. Escapes that would decode into a path separator, a relative segment, or the reserved `_pracht/` namespace fail the build.
|
|
45
|
+
|
|
46
|
+
## Client-side navigation
|
|
47
|
+
|
|
48
|
+
SSG loaders run at build time. For each full-hydration SSG route whose loader or route/shell `head()` metadata participates in client navigation, the build serializes route-state JSON under `dist/client/_pracht/state/` using bounded, collision-safe opaque path components, and the client router (compiled with `__PRACHT_STATIC_TARGET__`) fetches that file instead of the live route-state endpoint. Equivalent URL segment spellings (raw Unicode, lowercase percent escapes, and escaped unreserved characters) resolve to the same state file. Explicitly loaderless and headless routes fetch no Pracht state; loaderless routes with head metadata fetch static state for font-head fragments while their components and data remain browser-only. Navigation therefore stays client-side on a dumb static host.
|
|
49
|
+
|
|
50
|
+
## Output conventions
|
|
51
|
+
|
|
52
|
+
- Pages: `<path>/index.html` at the percent-decoded path (clean URLs — hosts must serve `index.html` for directory URLs). `pracht preview` decodes request segments to the same filesystem spelling.
|
|
53
|
+
- Route state: bounded, collision-safe opaque `.json` files under `_pracht/state/` (`/` uses `_pracht/state/index.json`). Files copied from `public/` may not occupy a generated state path; the build rejects them instead of overwriting them.
|
|
54
|
+
- `404.html`: the app's `notFound` page, rendered independently of ordinary route matching at build time (GitHub Pages / S3 error-document convention).
|
|
55
|
+
- `200.html` (opt-in via `staticAdapter({ fallback: "200.html" })`): SPA fallback document for hosts that can rewrite unmatched URLs; required for deep links into dynamic `render: "spa"` routes.
|
|
56
|
+
|
|
57
|
+
Files copied from `public/` or emitted by Vite may not occupy the generated
|
|
58
|
+
`404.html` or configured fallback path, including a case- or
|
|
59
|
+
Unicode-normalization-equivalent spelling; the build rejects those portable
|
|
60
|
+
collisions instead of overwriting existing output.
|
|
61
|
+
|
|
62
|
+
One fallback file is shared by every rewritten URL, so it cannot evaluate a
|
|
63
|
+
dynamic route or shell `head()` export. When those exports exist, provide
|
|
64
|
+
explicit generic metadata shared by every fallback URL:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
staticAdapter({
|
|
68
|
+
fallback: "200.html",
|
|
69
|
+
fallbackHead: { title: "My app", meta: [{ name: "robots", content: "noindex" }] },
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The build fails closed if a dynamic SPA route, its shell, or the not-found page
|
|
74
|
+
exports `head()` and `fallbackHead` is omitted.
|
|
75
|
+
|
|
76
|
+
Fonts in `fallbackHead` remain registered while the fallback commits a
|
|
77
|
+
loaderless dynamic SPA route.
|
|
78
|
+
|
|
79
|
+
The fallback only boots matched SPA routes. Paths matching a dynamic SSG route but omitted by `getStaticPaths()` render the app's `notFound` page rather than running without build-time loader state. When the fallback renders `notFound`, it reuses the loader data or handled error state serialized into `404.html`; the loader does not run again.
|
|
80
|
+
|
|
81
|
+
Fallback names may not collide with `index.html` or `404.html`, including case variants on case-insensitive filesystems, use a Windows reserved device name, or exceed the portable 255-byte/code-unit component limit.
|
|
82
|
+
Prerendered pages must also map to distinct portable filesystem paths. The build rejects duplicate, case-folded, and Unicode-normalization-equivalent outputs; Windows-invalid or overlong filename components; file/directory conflicts such as `/` with `/index.html`; and route directories that occupy `404.html` or the configured fallback file path before writing any page.
|
|
83
|
+
|
|
84
|
+
A host rewrite to the fallback answers unknown URLs with status 200, so they become soft 404s even though the client renders the `notFound` page. Skip `fallback` when correct 404 statuses matter more than deep links into dynamic SPA routes.
|
|
85
|
+
|
|
86
|
+
See `docs/ADAPTERS.md` in the repository for the full documentation, host configuration notes, and limitations.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { HeadMetadata } from "@pracht/core";
|
|
2
|
+
import { PrachtAdapter } from "@pracht/vite-plugin";
|
|
3
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
4
|
+
|
|
5
|
+
//#region src/static-entry.d.ts
|
|
6
|
+
interface StaticAdapterOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Additionally emit a SPA fallback document at `dist/client/<fallback>`
|
|
9
|
+
* (conventionally `"200.html"`). Configure your static host to rewrite
|
|
10
|
+
* unmatched URLs to it so deep links into non-prerendered paths (dynamic
|
|
11
|
+
* `render: "spa"` routes) boot the client router. Without a host rewrite
|
|
12
|
+
* the file is inert.
|
|
13
|
+
*/
|
|
14
|
+
fallback?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Metadata for the shared SPA fallback document. Every rewritten URL gets
|
|
17
|
+
* this same head because route, shell, and not-found `head()` exports run on
|
|
18
|
+
* the server and cannot be resolved for an arbitrary URL in a static document.
|
|
19
|
+
*/
|
|
20
|
+
fallbackHead?: HeadMetadata;
|
|
21
|
+
/** Port used by `pracht preview` / running the generated entry directly. Defaults to 3000. */
|
|
22
|
+
port?: number;
|
|
23
|
+
}
|
|
24
|
+
declare function createStaticServerEntryModule(options?: StaticAdapterOptions): string;
|
|
25
|
+
/**
|
|
26
|
+
* Create a pracht adapter for pure static export.
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* import { staticAdapter } from "@pracht/adapter-static";
|
|
30
|
+
* pracht({ adapter: staticAdapter() })
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* `pracht build` prerenders every route into `dist/client/` — deploy that
|
|
34
|
+
* directory to any static host. Build-time validation fails closed on
|
|
35
|
+
* anything that needs a server: `render: "ssr"` / `"isg"` routes, SPA
|
|
36
|
+
* loaders, request middleware, API routes, and network-exposed capabilities.
|
|
37
|
+
* SSG loaders run at build time; static SPA routes are loaderless.
|
|
38
|
+
*/
|
|
39
|
+
declare function staticAdapter(options?: StaticAdapterOptions): PrachtAdapter;
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/static-preview.d.ts
|
|
42
|
+
interface StaticPreviewHandlerOptions {
|
|
43
|
+
/** Directory to serve, usually `dist/client`. */
|
|
44
|
+
staticDir: string;
|
|
45
|
+
/**
|
|
46
|
+
* SPA fallback file name (e.g. `"200.html"`). When set and present in
|
|
47
|
+
* `staticDir`, unmatched URLs are answered with its contents and status
|
|
48
|
+
* 200 — mirroring a host-level rewrite rule. When unset, unmatched URLs
|
|
49
|
+
* are answered with `404.html` (status 404) when it exists.
|
|
50
|
+
*/
|
|
51
|
+
fallback?: string | null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A deliberately tiny static file server for `pracht preview` of static
|
|
55
|
+
* exports. It mirrors what a plain static host does — files, clean-URL
|
|
56
|
+
* `index.html` resolution, `404.html` for misses, an optional SPA fallback —
|
|
57
|
+
* and nothing else: no route matching, no loaders, no framework runtime.
|
|
58
|
+
* Production deploys should use a real static host; this exists so the build
|
|
59
|
+
* output can be exercised locally with one command.
|
|
60
|
+
*/
|
|
61
|
+
declare function createStaticPreviewHandler(options: StaticPreviewHandlerOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
62
|
+
//#endregion
|
|
63
|
+
export { type StaticAdapterOptions, type StaticPreviewHandlerOptions, createStaticPreviewHandler, createStaticServerEntryModule, staticAdapter };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { resolveStaticFile } from "@pracht/adapter-node";
|
|
4
|
+
//#region src/static-entry.ts
|
|
5
|
+
const FALLBACK_FILENAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*\.html$/;
|
|
6
|
+
const WINDOWS_RESERVED_FALLBACK_NAME_RE = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i;
|
|
7
|
+
const PORTABLE_FALLBACK_NAME_MAX_LENGTH = 255;
|
|
8
|
+
function assertValidStaticAdapterOptions(options) {
|
|
9
|
+
const fallback = options.fallback;
|
|
10
|
+
if (fallback === void 0) {
|
|
11
|
+
if (options.fallbackHead !== void 0) throw new Error("staticAdapter({ fallbackHead }) requires a fallback file.");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const normalizedFallback = fallback.toLowerCase();
|
|
15
|
+
if (normalizedFallback === "index.html" || normalizedFallback === "404.html") throw new Error(`staticAdapter({ fallback: ${JSON.stringify(fallback)} }) collides with a reserved output file — "index.html" is the root route and "404.html" is the rendered not-found page. Use "200.html".`);
|
|
16
|
+
if (!FALLBACK_FILENAME_RE.test(fallback)) throw new Error(`staticAdapter({ fallback }) expects a plain HTML file name such as "200.html", got ${JSON.stringify(fallback)}.`);
|
|
17
|
+
if (WINDOWS_RESERVED_FALLBACK_NAME_RE.test(fallback) || fallback.length > PORTABLE_FALLBACK_NAME_MAX_LENGTH || Buffer.byteLength(fallback, "utf-8") > PORTABLE_FALLBACK_NAME_MAX_LENGTH) throw new Error(`staticAdapter({ fallback: ${JSON.stringify(fallback)} }) is not a portable file name. Avoid Windows reserved device names and names longer than 255 bytes/code units.`);
|
|
18
|
+
}
|
|
19
|
+
function createStaticServerEntryModule(options = {}) {
|
|
20
|
+
assertValidStaticAdapterOptions(options);
|
|
21
|
+
const fallback = options.fallback ?? null;
|
|
22
|
+
const fallbackHead = options.fallbackHead ?? null;
|
|
23
|
+
const port = options.port ?? 3e3;
|
|
24
|
+
return [
|
|
25
|
+
"",
|
|
26
|
+
"// ---- @pracht/adapter-static ------------------------------------------",
|
|
27
|
+
"// A static export has no runtime server: dist/client is the deployable",
|
|
28
|
+
"// artifact. Everything below exists for `pracht build` (which imports",
|
|
29
|
+
"// this bundle to prerender) and `pracht preview` (which runs this file",
|
|
30
|
+
"// directly to serve dist/client locally).",
|
|
31
|
+
"",
|
|
32
|
+
`export const staticExportConfig = { fallback: ${JSON.stringify(fallback)}, fallbackHead: ${JSON.stringify(fallbackHead)} };`,
|
|
33
|
+
"",
|
|
34
|
+
"// Remove ordinary routes from the build-only not-found app so a broad",
|
|
35
|
+
"// dynamic pattern (`/:slug`, `/*`) cannot consume the synthetic request",
|
|
36
|
+
"// before the app-level notFound page runs. `hrefRoutes` keeps the real",
|
|
37
|
+
"// table available to `<Link route=…>`/`href()` in the shell and the",
|
|
38
|
+
"// not-found page — they are not matched, only resolved.",
|
|
39
|
+
"const staticNotFoundApp = { ...resolvedApp, routes: [], hrefRoutes: resolvedApp.routes };",
|
|
40
|
+
"",
|
|
41
|
+
"// Rendered to dist/client/404.html (the GitHub Pages / S3 error-document",
|
|
42
|
+
"// convention). Returns null when the app declares none.",
|
|
43
|
+
"export async function renderStaticNotFoundHtml() {",
|
|
44
|
+
" if (!resolvedApp.notFound) return null;",
|
|
45
|
+
" let renderError;",
|
|
46
|
+
" const response = await handlePrachtRequest({",
|
|
47
|
+
" app: staticNotFoundApp,",
|
|
48
|
+
" registry,",
|
|
49
|
+
" request: new Request(\"http://localhost/404.html\", { method: \"GET\" }),",
|
|
50
|
+
" clientEntryUrl: clientEntryUrl ?? undefined,",
|
|
51
|
+
" islandsEntryUrl: islandsEntryUrl ?? undefined,",
|
|
52
|
+
" islandsBootstrapRequired,",
|
|
53
|
+
" cssManifest,",
|
|
54
|
+
" jsManifest,",
|
|
55
|
+
" onRouteError: (error) => {",
|
|
56
|
+
" renderError = error;",
|
|
57
|
+
" },",
|
|
58
|
+
" });",
|
|
59
|
+
" const contentType = response.headers.get(\"content-type\") ?? \"\";",
|
|
60
|
+
" if (response.status !== 404) {",
|
|
61
|
+
" const location = response.headers.get(\"location\");",
|
|
62
|
+
" const detail = location ? ` (redirect: ${location})` : \"\";",
|
|
63
|
+
" throw new Error(",
|
|
64
|
+
" `Static export failed to render the notFound page: status ${response.status}${detail}. Make its loader succeed at build time or use a serverful adapter.` +",
|
|
65
|
+
" describeRenderError(renderError),",
|
|
66
|
+
" renderError === undefined ? undefined : { cause: renderError },",
|
|
67
|
+
" );",
|
|
68
|
+
" }",
|
|
69
|
+
" if (!contentType.includes(\"text/html\")) {",
|
|
70
|
+
" throw new Error(`Static export failed to render the notFound page as HTML (content-type: ${contentType || \"missing\"}).`);",
|
|
71
|
+
" }",
|
|
72
|
+
" return await response.text();",
|
|
73
|
+
"}",
|
|
74
|
+
"",
|
|
75
|
+
"export function renderStaticFallbackHtml(notFoundState) {",
|
|
76
|
+
" return buildStaticFallbackHtml({",
|
|
77
|
+
" clientEntryUrl: clientEntryUrl ?? undefined,",
|
|
78
|
+
" head: staticExportConfig.fallbackHead ?? undefined,",
|
|
79
|
+
" notFoundData: notFoundState?.data,",
|
|
80
|
+
" notFoundError: notFoundState?.error ?? null,",
|
|
81
|
+
" });",
|
|
82
|
+
"}",
|
|
83
|
+
"",
|
|
84
|
+
"const entryHref = process.argv[1] ? pathToFileURL(process.argv[1]).href : null;",
|
|
85
|
+
"if (entryHref && import.meta.url === entryHref) {",
|
|
86
|
+
" const serverDir = dirname(fileURLToPath(import.meta.url));",
|
|
87
|
+
" const handler = createStaticPreviewHandler({",
|
|
88
|
+
" staticDir: resolve(serverDir, \"../client\"),",
|
|
89
|
+
" fallback: staticExportConfig.fallback,",
|
|
90
|
+
" });",
|
|
91
|
+
" const server = createServer(handler);",
|
|
92
|
+
` const port = Number(process.env.PORT ?? ${port});`,
|
|
93
|
+
" server.listen(port, () => {",
|
|
94
|
+
" console.log(`pracht static preview listening on http://localhost:${port}`);",
|
|
95
|
+
" });",
|
|
96
|
+
"}",
|
|
97
|
+
""
|
|
98
|
+
].join("\n");
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Create a pracht adapter for pure static export.
|
|
102
|
+
*
|
|
103
|
+
* ```ts
|
|
104
|
+
* import { staticAdapter } from "@pracht/adapter-static";
|
|
105
|
+
* pracht({ adapter: staticAdapter() })
|
|
106
|
+
* ```
|
|
107
|
+
*
|
|
108
|
+
* `pracht build` prerenders every route into `dist/client/` — deploy that
|
|
109
|
+
* directory to any static host. Build-time validation fails closed on
|
|
110
|
+
* anything that needs a server: `render: "ssr"` / `"isg"` routes, SPA
|
|
111
|
+
* loaders, request middleware, API routes, and network-exposed capabilities.
|
|
112
|
+
* SSG loaders run at build time; static SPA routes are loaderless.
|
|
113
|
+
*/
|
|
114
|
+
function staticAdapter(options = {}) {
|
|
115
|
+
assertValidStaticAdapterOptions(options);
|
|
116
|
+
return {
|
|
117
|
+
id: "static",
|
|
118
|
+
staticTarget: true,
|
|
119
|
+
serverImports: [
|
|
120
|
+
"import { createServer } from \"node:http\";",
|
|
121
|
+
"import { dirname, resolve } from \"node:path\";",
|
|
122
|
+
"import { fileURLToPath, pathToFileURL } from \"node:url\";",
|
|
123
|
+
"import { resolveApp, resolveApiRoutes, handlePrachtRequest, buildStaticFallbackHtml, describeRenderError } from \"@pracht/core/server\";",
|
|
124
|
+
"import { createStaticPreviewHandler } from \"@pracht/adapter-static\";"
|
|
125
|
+
].join("\n"),
|
|
126
|
+
createServerEntryModule() {
|
|
127
|
+
return createStaticServerEntryModule(options);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/static-preview.ts
|
|
133
|
+
/**
|
|
134
|
+
* A deliberately tiny static file server for `pracht preview` of static
|
|
135
|
+
* exports. It mirrors what a plain static host does — files, clean-URL
|
|
136
|
+
* `index.html` resolution, `404.html` for misses, an optional SPA fallback —
|
|
137
|
+
* and nothing else: no route matching, no loaders, no framework runtime.
|
|
138
|
+
* Production deploys should use a real static host; this exists so the build
|
|
139
|
+
* output can be exercised locally with one command.
|
|
140
|
+
*/
|
|
141
|
+
function createStaticPreviewHandler(options) {
|
|
142
|
+
const staticDir = resolve(options.staticDir);
|
|
143
|
+
const fallback = options.fallback ?? null;
|
|
144
|
+
return async function handleStaticPreviewRequest(req, res) {
|
|
145
|
+
const method = req.method ?? "GET";
|
|
146
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
147
|
+
res.writeHead(405, {
|
|
148
|
+
"content-type": "text/plain; charset=utf-8",
|
|
149
|
+
allow: "GET, HEAD"
|
|
150
|
+
});
|
|
151
|
+
res.end("Method not allowed");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
let pathname;
|
|
155
|
+
try {
|
|
156
|
+
const encodedPathname = new URL(req.url ?? "/", "http://localhost").pathname;
|
|
157
|
+
pathname = decodeStaticPreviewPathname(encodedPathname);
|
|
158
|
+
} catch {
|
|
159
|
+
res.writeHead(400, { "content-type": "text/plain; charset=utf-8" });
|
|
160
|
+
res.end("Bad request");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const file = await resolveStaticFile(staticDir, pathname);
|
|
164
|
+
if (file) {
|
|
165
|
+
await sendFile(res, method, file.filePath, file.contentType, file.cacheControl, 200);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (fallback) {
|
|
169
|
+
const fallbackFile = await resolveStaticFile(staticDir, `/${fallback}`);
|
|
170
|
+
if (fallbackFile?.filePath === resolve(staticDir, fallback)) {
|
|
171
|
+
await sendFile(res, method, fallbackFile.filePath, "text/html; charset=utf-8", "no-cache", 200);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const notFoundFile = await resolveStaticFile(staticDir, "/404.html");
|
|
176
|
+
if (notFoundFile?.filePath === resolve(staticDir, "404.html")) {
|
|
177
|
+
await sendFile(res, method, notFoundFile.filePath, "text/html; charset=utf-8", "no-cache", 404);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
181
|
+
res.end("Not found");
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Decode each URL segment into the filesystem spelling emitted by the static
|
|
186
|
+
* exporter without allowing an escape to introduce path structure.
|
|
187
|
+
*/
|
|
188
|
+
function decodeStaticPreviewPathname(pathname) {
|
|
189
|
+
return pathname.split("/").map((segment) => {
|
|
190
|
+
const decoded = decodeURIComponent(segment);
|
|
191
|
+
if (decoded === "." || decoded === ".." || decoded.includes("/") || decoded.includes("\\") || decoded.includes("\0")) throw new Error("Unsafe encoded static path segment.");
|
|
192
|
+
return decoded;
|
|
193
|
+
}).join("/");
|
|
194
|
+
}
|
|
195
|
+
async function sendFile(res, method, filePath, contentType, cacheControl, status) {
|
|
196
|
+
let body;
|
|
197
|
+
try {
|
|
198
|
+
body = await readFile(filePath);
|
|
199
|
+
} catch {
|
|
200
|
+
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
201
|
+
res.end("Not found");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
res.writeHead(status, {
|
|
205
|
+
"content-type": contentType,
|
|
206
|
+
"cache-control": cacheControl,
|
|
207
|
+
"content-length": body.byteLength
|
|
208
|
+
});
|
|
209
|
+
res.end(method === "HEAD" ? void 0 : body);
|
|
210
|
+
}
|
|
211
|
+
//#endregion
|
|
212
|
+
export { createStaticPreviewHandler, createStaticServerEntryModule, staticAdapter };
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pracht/adapter-static",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Static-export adapter for Pracht: prerender every route and deploy dist/client to any static host with zero server.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pracht",
|
|
7
|
+
"preact",
|
|
8
|
+
"static",
|
|
9
|
+
"ssg",
|
|
10
|
+
"export",
|
|
11
|
+
"adapter",
|
|
12
|
+
"github-pages",
|
|
13
|
+
"vite"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"homepage": "https://github.com/JoviDeCroock/pracht/tree/main/packages/adapter-static",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/JoviDeCroock/pracht/issues"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/JoviDeCroock/pracht",
|
|
23
|
+
"directory": "packages/adapter-static"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.mts",
|
|
32
|
+
"default": "./dist/index.mjs"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"provenance": false,
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@pracht/adapter-node": "0.3.9",
|
|
41
|
+
"@pracht/core": "0.13.0"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsdown"
|
|
45
|
+
}
|
|
46
|
+
}
|