@pracht/core 0.5.0 → 0.6.1
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 +1 -0
- package/dist/index.mjs +24 -18
- package/package.json +14 -1
package/dist/index.d.mts
CHANGED
|
@@ -203,6 +203,7 @@ interface RouteModule<TContext = any, TLoader extends LoaderLike = undefined> {
|
|
|
203
203
|
interface ShellModule<TContext = any> {
|
|
204
204
|
Shell: FunctionComponent<ShellProps>;
|
|
205
205
|
Loading?: FunctionComponent;
|
|
206
|
+
ErrorBoundary?: FunctionComponent<ErrorBoundaryProps>;
|
|
206
207
|
head?: (args: BaseRouteArgs<TContext>) => MaybePromise<HeadMetadata>;
|
|
207
208
|
headers?: (args: BaseRouteArgs<TContext>) => MaybePromise<HeadersInit>;
|
|
208
209
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -154,6 +154,7 @@ function parseRouteSegments(path) {
|
|
|
154
154
|
type: "param",
|
|
155
155
|
name: segment.slice(1)
|
|
156
156
|
};
|
|
157
|
+
assertSafeStaticRouteSegment(segment);
|
|
157
158
|
return {
|
|
158
159
|
type: "static",
|
|
159
160
|
value: segment
|
|
@@ -163,6 +164,10 @@ function parseRouteSegments(path) {
|
|
|
163
164
|
function splitPathSegments(path) {
|
|
164
165
|
return normalizeRoutePath(path).split("/").filter(Boolean);
|
|
165
166
|
}
|
|
167
|
+
function assertSafeStaticRouteSegment(segment) {
|
|
168
|
+
if (segment === "." || segment === "..") throw new Error(`Unsafe static route segment "${segment}" is not allowed.`);
|
|
169
|
+
if (segment.includes("\0") || /[\r\n\\]/.test(segment)) throw new Error(`Unsafe static route segment "${segment}" contains a forbidden character.`);
|
|
170
|
+
}
|
|
166
171
|
function mergeRoutePaths(prefix, path) {
|
|
167
172
|
if (!path) return normalizeRoutePath(prefix);
|
|
168
173
|
const normalizedPrefix = normalizeRoutePath(prefix);
|
|
@@ -179,19 +184,18 @@ function normalizeRoutePath(path) {
|
|
|
179
184
|
function buildPathFromSegments(segments, params) {
|
|
180
185
|
return normalizeRoutePath("/" + segments.map((segment) => {
|
|
181
186
|
if (segment.type === "static") return segment.value;
|
|
182
|
-
if (segment.type === "param") return
|
|
183
|
-
return (params[segment.name] ?? params["*"] ?? "").split("/").map((part) =>
|
|
187
|
+
if (segment.type === "param") return encodeDynamicPathSegment(params[segment.name] ?? "");
|
|
188
|
+
return (params[segment.name] ?? params["*"] ?? "").split("/").map((part) => encodeDynamicPathSegment(part)).join("/");
|
|
184
189
|
}).join("/"));
|
|
185
190
|
}
|
|
186
191
|
/**
|
|
187
|
-
* Encode
|
|
188
|
-
* leaves unreserved characters (including `.`) intact,
|
|
189
|
-
*
|
|
190
|
-
*
|
|
192
|
+
* Encode one dynamic URL path segment for SSG/ISG output. `encodeURIComponent`
|
|
193
|
+
* leaves unreserved characters (including `.`) intact, and even percent-encoded
|
|
194
|
+
* dot segments are normalized by URL parsers. Reject exact `.` / `..` segments
|
|
195
|
+
* instead of allowing them to reach filesystem output path construction.
|
|
191
196
|
*/
|
|
192
|
-
function
|
|
193
|
-
if (part === ".")
|
|
194
|
-
if (part === "..") return "%2E%2E";
|
|
197
|
+
function encodeDynamicPathSegment(part) {
|
|
198
|
+
if (part === "." || part === "..") throw new Error(`Unsafe dynamic route param segment "${part}" is not allowed.`);
|
|
195
199
|
return encodeURIComponent(part);
|
|
196
200
|
}
|
|
197
201
|
/**
|
|
@@ -1170,25 +1174,26 @@ async function renderRouteErrorResponse(options) {
|
|
|
1170
1174
|
status: routeError.status
|
|
1171
1175
|
})
|
|
1172
1176
|
} : routeError;
|
|
1173
|
-
if (
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
+
if (options.isRouteStateRequest) return jsonErrorResponse(routeErrorWithDiagnostics, { isRouteStateRequest: true });
|
|
1178
|
+
const shellModule = options.shellModule ?? (options.shellFile ? await resolveRegistryModule(options.options.registry?.shellModules, options.shellFile) : void 0);
|
|
1179
|
+
const ErrorBoundary = options.routeModule?.ErrorBoundary ?? shellModule?.ErrorBoundary;
|
|
1180
|
+
if (!ErrorBoundary) {
|
|
1181
|
+
const message = routeErrorWithDiagnostics.status >= 500 && !exposeDetails ? "Internal Server Error" : routeErrorWithDiagnostics.message;
|
|
1182
|
+
const diagnostics = exposeDetails && routeErrorWithDiagnostics.diagnostics ? `\n\n${JSON.stringify(routeErrorWithDiagnostics.diagnostics, null, 2)}` : "";
|
|
1183
|
+
return withDefaultSecurityHeaders(new Response(`${message}${diagnostics}`, {
|
|
1177
1184
|
status: routeErrorWithDiagnostics.status,
|
|
1178
1185
|
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
1179
1186
|
}));
|
|
1180
1187
|
}
|
|
1181
|
-
if (options.isRouteStateRequest) return jsonErrorResponse(routeErrorWithDiagnostics, { isRouteStateRequest: true });
|
|
1182
|
-
const shellModule = options.shellModule ?? (options.shellFile ? await resolveRegistryModule(options.options.registry?.shellModules, options.shellFile) : void 0);
|
|
1183
1188
|
const head = shellModule?.head ? await shellModule.head(options.routeArgs) : {};
|
|
1184
1189
|
const documentHeaders = await mergeDocumentHeaders(shellModule, void 0, options.routeArgs, void 0);
|
|
1185
1190
|
const cssUrls = resolvePageCssUrls(options.options.cssManifest, options.shellFile, options.routeArgs.route.file);
|
|
1186
1191
|
const modulePreloadUrls = resolvePageJsUrls(options.options.jsManifest, options.shellFile, options.routeArgs.route.file);
|
|
1187
1192
|
const renderToString = await getRenderToStringAsync();
|
|
1188
|
-
const
|
|
1193
|
+
const Boundary = ErrorBoundary;
|
|
1189
1194
|
const Shell = shellModule?.Shell;
|
|
1190
1195
|
const errorValue = deserializeRouteError(routeErrorWithDiagnostics);
|
|
1191
|
-
const componentTree = Shell ? h(Shell, null, h(
|
|
1196
|
+
const componentTree = Shell ? h(Shell, null, h(Boundary, { error: errorValue })) : h(Boundary, { error: errorValue });
|
|
1192
1197
|
return htmlResponse(buildHtmlDocument({
|
|
1193
1198
|
head,
|
|
1194
1199
|
body: await renderToString(h(PrachtRuntimeProvider, {
|
|
@@ -1721,7 +1726,8 @@ async function initClientRouter(options) {
|
|
|
1721
1726
|
const resolvedShell = await (shellModPromise ?? startShellImport(match));
|
|
1722
1727
|
if (resolvedShell) Shell = resolvedShell.Shell;
|
|
1723
1728
|
const DefaultComponent = typeof routeMod.default === "function" ? routeMod.default : void 0;
|
|
1724
|
-
const
|
|
1729
|
+
const ErrorBoundary = routeMod.ErrorBoundary ?? resolvedShell?.ErrorBoundary;
|
|
1730
|
+
const Component = state.error ? ErrorBoundary : routeMod.Component ?? DefaultComponent;
|
|
1725
1731
|
if (!Component) return null;
|
|
1726
1732
|
const componentProps = state.error ? { error: deserializeRouteError(state.error) } : {
|
|
1727
1733
|
data: state.data,
|
package/package.json
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pracht/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
|
+
"description": "Core runtime for Pracht, a full-stack Preact framework with per-route SSR, SSG, ISG, SPA, loaders, and API routes.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pracht",
|
|
7
|
+
"preact",
|
|
8
|
+
"framework",
|
|
9
|
+
"full-stack",
|
|
10
|
+
"ssr",
|
|
11
|
+
"ssg",
|
|
12
|
+
"isg",
|
|
13
|
+
"routing",
|
|
14
|
+
"loaders",
|
|
15
|
+
"api-routes"
|
|
16
|
+
],
|
|
4
17
|
"license": "MIT",
|
|
5
18
|
"homepage": "https://github.com/JoviDeCroock/pracht/tree/main/packages/framework",
|
|
6
19
|
"bugs": {
|