xpine 0.0.56 → 0.0.59
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/.claude/settings.local.json +32 -0
- package/README.md +93 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +269 -106
- package/dist/src/auth.d.ts.map +1 -1
- package/dist/src/csrf.d.ts +16 -0
- package/dist/src/csrf.d.ts.map +1 -0
- package/dist/src/express.d.ts +5 -0
- package/dist/src/express.d.ts.map +1 -1
- package/dist/src/scripts/build.d.ts.map +1 -1
- package/dist/src/scripts/xpine-build.js +6 -2
- package/dist/src/scripts/xpine-dev.js +15 -21
- package/dist/src/util/html.d.ts +9 -1
- package/dist/src/util/html.d.ts.map +1 -1
- package/dist/src/util/regex.d.ts +2 -0
- package/dist/src/util/regex.d.ts.map +1 -1
- package/dist/types.d.ts +4 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/types.ts +5 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(npm run *)",
|
|
5
|
+
"Bash(PORT=8888 node ./dist/server/run/prod.js)",
|
|
6
|
+
"Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:8888/)",
|
|
7
|
+
"Bash(break)",
|
|
8
|
+
"Bash(curl -s -o /dev/null -w \"status=%{http_code}\\\\n\" http://localhost:8888/blog/technology/devops/my-blog-post)",
|
|
9
|
+
"Bash(curl -s http://localhost:8888/blog/technology/devops/my-blog-post)",
|
|
10
|
+
"Bash(curl -s -o /dev/null -w \"status=%{http_code}\\\\n\" http://localhost:8888/blog/preview/some/new/post)",
|
|
11
|
+
"Bash(curl -s http://localhost:8888/blog/preview/some/new/post)",
|
|
12
|
+
"Bash(curl -s -o /dev/null -w \"status=%{http_code}\\\\n\" http://localhost:8888/blog/totally/unknown/path)",
|
|
13
|
+
"Bash(curl -s http://localhost:8888/blog/totally/unknown/path)",
|
|
14
|
+
"Bash(curl -s -o /dev/null -w \"status=%{http_code}\\\\n\" http://localhost:8888/some-totally-random-route-xyz)",
|
|
15
|
+
"Bash(curl -s http://localhost:8888/some-totally-random-route-xyz)",
|
|
16
|
+
"Bash(curl -s http://localhost:8888/some-random-xyz)",
|
|
17
|
+
"Bash(curl -s -o /dev/null -w \"status=%{http_code}\\\\n\" http://localhost:8888/blog/nope)",
|
|
18
|
+
"Bash(curl -s http://localhost:8888/blog/nope)",
|
|
19
|
+
"Bash(PORT=8888 npx playwright test app-build --grep \"multi-segment\")",
|
|
20
|
+
"Bash(PORT=8888 npx playwright test)",
|
|
21
|
+
"Bash(npx eslint *)",
|
|
22
|
+
"Bash(echo \"=== exit: $? ===\")",
|
|
23
|
+
"Bash(git stash *)",
|
|
24
|
+
"Bash(PORT=8888 npx playwright test app-build --grep \"traverse\")",
|
|
25
|
+
"Bash(node -e \"console.log\\(require\\('./node_modules/jsonwebtoken/package.json'\\).version\\)\")",
|
|
26
|
+
"Bash(JWT_PRIVATE_KEY=test-secret-123 node --input-type=module -e ' *)",
|
|
27
|
+
"Bash(node --input-type=module -e ' *)",
|
|
28
|
+
"Bash(CSRF_SECRET='csrf-secret-xyz' node /tmp/csrf-test.mjs)",
|
|
29
|
+
"Bash(CSRF_SECRET='csrf-secret-xyz' node csrf-test.mjs)"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
}
|
package/README.md
CHANGED
|
@@ -155,6 +155,47 @@ You can create catch all routes by naming the file \_all\_.(jsx|tsx|js|ts). You
|
|
|
155
155
|
|
|
156
156
|
You can get the route param in your function with req.params[0], such as how express handles catch all routes.
|
|
157
157
|
|
|
158
|
+
### Multi-segment dynamic routes (`[...slug]`)
|
|
159
|
+
|
|
160
|
+
Catch all (`_all_`) routes register a single Express wildcard, which means they match _every_ path under their prefix — including paths that should not exist. For deeply nested but known dynamic routes (e.g. a blog post at `/blog/technology/devops/my-blog-post`), use a multi-segment dynamic param instead. Name the file `[...slug].(jsx|tsx|js|ts)`; the captured value (which may contain slashes) is available as `req.params.slug`.
|
|
161
|
+
|
|
162
|
+
Instead of a wildcard, XPine registers an **explicit Express route for each slug** returned by the `staticPaths` config function, so unknown paths safely fall through to your 404 page:
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
// /src/pages/blog/+config.ts
|
|
166
|
+
export default {
|
|
167
|
+
staticPaths() {
|
|
168
|
+
// Slugs can come from a CMS/database
|
|
169
|
+
return [
|
|
170
|
+
{ slug: 'technology/devops/my-blog-post' },
|
|
171
|
+
];
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
// /src/pages/blog/[...slug].tsx
|
|
178
|
+
import { PageProps } from 'xpine/dist/types';
|
|
179
|
+
|
|
180
|
+
export default function BlogPost({ req }: PageProps) {
|
|
181
|
+
return <div>{req.params.slug}</div>;
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Each slug from `staticPaths` is also statically generated at build time. To allow slugs that are not known at build time (without falling back to an unsafe catch-all), add an `isValid` function to the config. It receives the requested slug and runs at request time; return `false` (the default for unknown slugs) to fall through to the 404 handler:
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
export default {
|
|
189
|
+
staticPaths() {
|
|
190
|
+
return [{ slug: 'technology/devops/my-blog-post' }];
|
|
191
|
+
},
|
|
192
|
+
// Resolve newly-published slugs without a rebuild
|
|
193
|
+
async isValid(slug, req) {
|
|
194
|
+
return await postExists(slug);
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
```
|
|
198
|
+
|
|
158
199
|
### Route specific middleware
|
|
159
200
|
|
|
160
201
|
If you need route specific middleware, e.g. for file uploads, you can specify a `routeMiddleware` function in a config variable in the endpoint file:
|
|
@@ -168,6 +209,58 @@ export const config = {
|
|
|
168
209
|
}
|
|
169
210
|
```
|
|
170
211
|
|
|
212
|
+
### CSRF protection
|
|
213
|
+
|
|
214
|
+
Because authentication is cookie based, state-changing requests (POST/PUT/PATCH/DELETE) are exposed to cross-site request forgery. Enable the built-in guard by setting `csrf` in your `xpine.config.mjs`:
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
export default {
|
|
218
|
+
csrf: true,
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
It uses a stateless **signed double-submit cookie**. On safe requests (GET/HEAD/OPTIONS) it sets a `csrfToken` cookie (readable by client JS). On state-changing requests it requires that same token to be echoed back in an `x-csrf-token` header (or a `_csrf` form field) — a cross-site attacker can make the browser send the cookie but cannot read it or set the header, so forged requests are rejected with a 403. The token is HMAC-signed so it can't be forged even by an attacker who can plant a cookie.
|
|
223
|
+
|
|
224
|
+
Set a secret via the `CSRF_SECRET` env var (it falls back to `JWT_PRIVATE_KEY`). The server fails to start if neither is set.
|
|
225
|
+
|
|
226
|
+
For `fetch`/Alpine requests, read the cookie and send it back:
|
|
227
|
+
```
|
|
228
|
+
const token = document.cookie.split('; ').find(c => c.startsWith('csrfToken='))?.split('=')[1];
|
|
229
|
+
await fetch('/api/thing', {
|
|
230
|
+
method: 'POST',
|
|
231
|
+
headers: { 'x-csrf-token': decodeURIComponent(token) },
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
For server-rendered HTML forms, the token is available on `res.locals.csrfToken` (pages receive `res`); render it as a hidden `<input name="_csrf">`.
|
|
235
|
+
|
|
236
|
+
You can customize the cookie/header/field names, cookie attributes, and skip specific path prefixes (e.g. signed webhooks):
|
|
237
|
+
```
|
|
238
|
+
export default {
|
|
239
|
+
csrf: {
|
|
240
|
+
headerName: 'x-csrf-token',
|
|
241
|
+
cookie: { sameSite: 'strict', secure: true },
|
|
242
|
+
ignorePaths: ['/api/webhooks/'],
|
|
243
|
+
},
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### HTML escaping (XSS protection)
|
|
248
|
+
|
|
249
|
+
Values interpolated into JSX are HTML-escaped by default, so rendering user input is safe:
|
|
250
|
+
```
|
|
251
|
+
// req.params.slug = '<img src=x onerror=alert(1)>'
|
|
252
|
+
<div>{req.params.slug}</div> // renders <img src=x onerror=alert(1)>
|
|
253
|
+
```
|
|
254
|
+
Nested components and elements are not re-escaped, and attribute values are escaped too. Text inside `<script>`/`<style>` is left raw (it isn't HTML) — never interpolate untrusted data there.
|
|
255
|
+
|
|
256
|
+
If you have trusted HTML that should render as-is, opt out explicitly with `raw()`:
|
|
257
|
+
```
|
|
258
|
+
import { raw } from 'xpine';
|
|
259
|
+
|
|
260
|
+
<div>{raw(trustedHtmlString)}</div>
|
|
261
|
+
```
|
|
262
|
+
Only use `raw()` with HTML you control — never with user input.
|
|
263
|
+
|
|
171
264
|
### Static Site Generation
|
|
172
265
|
|
|
173
266
|
Generate path specific static pages by specifying in the config of either the page's file, such as `/src/pages/about.tsx` with a config export:
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export * from './src/util/env';
|
|
|
4
4
|
export * from './src/util/get-config';
|
|
5
5
|
export * from './src/scripts/build';
|
|
6
6
|
export * from './src/auth';
|
|
7
|
+
export * from './src/csrf';
|
|
7
8
|
export * from './src/util/html';
|
|
8
9
|
export * from './src/context';
|
|
9
10
|
export * from './src/util/web';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -326,6 +326,9 @@ var regex_default = {
|
|
|
326
326
|
configFile: /\+config\.[tj]sx?/g,
|
|
327
327
|
dynamicRoutes: /(\[)(.*?)(\])/g,
|
|
328
328
|
isDynamicRoute: /\[(.*)\]/g,
|
|
329
|
+
// Multi-segment (slash-containing) dynamic route, e.g. [...slug]
|
|
330
|
+
spreadRoute: /\[\.\.\.(.*?)\]/g,
|
|
331
|
+
isSpreadRoute: /\[\.\.\..*?\]/,
|
|
329
332
|
endsWithTSX: /\.tsx$/,
|
|
330
333
|
endsWithJSX: /\.jsx$/,
|
|
331
334
|
endsWithJs: /\.js$/,
|
|
@@ -467,6 +470,12 @@ import { globSync } from "glob";
|
|
|
467
470
|
// src/auth.ts
|
|
468
471
|
import jsonwebtoken from "jsonwebtoken";
|
|
469
472
|
var { verify, sign } = jsonwebtoken;
|
|
473
|
+
var JWT_ALGORITHM = "HS256";
|
|
474
|
+
function getSecret() {
|
|
475
|
+
const secret = process.env.JWT_PRIVATE_KEY;
|
|
476
|
+
if (!secret) throw new Error("JWT_PRIVATE_KEY is not set");
|
|
477
|
+
return secret;
|
|
478
|
+
}
|
|
470
479
|
async function signUser(email, username) {
|
|
471
480
|
return new Promise((resolve, reject) => {
|
|
472
481
|
sign(
|
|
@@ -476,11 +485,10 @@ async function signUser(email, username) {
|
|
|
476
485
|
username
|
|
477
486
|
}
|
|
478
487
|
},
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
{ expiresIn: "30d" },
|
|
488
|
+
getSecret(),
|
|
489
|
+
{ expiresIn: "30d", algorithm: JWT_ALGORITHM },
|
|
482
490
|
(err, token) => {
|
|
483
|
-
if (err) reject(err);
|
|
491
|
+
if (err) return reject(err);
|
|
484
492
|
resolve(token);
|
|
485
493
|
}
|
|
486
494
|
);
|
|
@@ -488,8 +496,8 @@ async function signUser(email, username) {
|
|
|
488
496
|
}
|
|
489
497
|
async function verifyUser(token) {
|
|
490
498
|
return new Promise((resolve, reject) => {
|
|
491
|
-
verify(token,
|
|
492
|
-
if (err) reject(err);
|
|
499
|
+
verify(token, getSecret(), { algorithms: [JWT_ALGORITHM] }, (err, authorizedData) => {
|
|
500
|
+
if (err) return reject(err);
|
|
493
501
|
resolve(authorizedData);
|
|
494
502
|
});
|
|
495
503
|
});
|
|
@@ -506,8 +514,163 @@ import requestIP from "request-ip";
|
|
|
506
514
|
import fs5 from "fs-extra";
|
|
507
515
|
import path5 from "path";
|
|
508
516
|
import EventEmitter from "events";
|
|
517
|
+
|
|
518
|
+
// src/csrf.ts
|
|
519
|
+
import crypto from "crypto";
|
|
520
|
+
var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
521
|
+
function resolveSecret(options) {
|
|
522
|
+
const secret = options?.secret || process.env.CSRF_SECRET || process.env.JWT_PRIVATE_KEY;
|
|
523
|
+
if (!secret) throw new Error("CSRF secret is not set (set CSRF_SECRET or JWT_PRIVATE_KEY)");
|
|
524
|
+
return secret;
|
|
525
|
+
}
|
|
526
|
+
function hmac(value, secret) {
|
|
527
|
+
return crypto.createHmac("sha256", secret).update(value).digest("base64url");
|
|
528
|
+
}
|
|
529
|
+
function issueToken(secret) {
|
|
530
|
+
const random = crypto.randomBytes(32).toString("base64url");
|
|
531
|
+
return `${random}.${hmac(random, secret)}`;
|
|
532
|
+
}
|
|
533
|
+
function isValidToken(token, secret) {
|
|
534
|
+
if (typeof token !== "string" || !token) return false;
|
|
535
|
+
const idx = token.lastIndexOf(".");
|
|
536
|
+
if (idx <= 0 || idx === token.length - 1) return false;
|
|
537
|
+
const random = token.slice(0, idx);
|
|
538
|
+
const signature = token.slice(idx + 1);
|
|
539
|
+
return timingSafeEqual(signature, hmac(random, secret));
|
|
540
|
+
}
|
|
541
|
+
function timingSafeEqual(a, b) {
|
|
542
|
+
const ab = Buffer.from(String(a));
|
|
543
|
+
const bb = Buffer.from(String(b));
|
|
544
|
+
if (ab.length !== bb.length) return false;
|
|
545
|
+
return crypto.timingSafeEqual(ab, bb);
|
|
546
|
+
}
|
|
547
|
+
function readSubmittedToken(req, headerName, fieldName) {
|
|
548
|
+
const header = req.headers[headerName];
|
|
549
|
+
const fromHeader = Array.isArray(header) ? header[0] : header;
|
|
550
|
+
if (fromHeader) return fromHeader;
|
|
551
|
+
if (req.body && typeof req.body[fieldName] === "string") return req.body[fieldName];
|
|
552
|
+
return "";
|
|
553
|
+
}
|
|
554
|
+
function createCsrfMiddleware(options) {
|
|
555
|
+
const secret = resolveSecret(options);
|
|
556
|
+
const cookieName = options?.cookieName || "csrfToken";
|
|
557
|
+
const headerName = (options?.headerName || "x-csrf-token").toLowerCase();
|
|
558
|
+
const fieldName = options?.fieldName || "_csrf";
|
|
559
|
+
const ignorePaths = options?.ignorePaths || [];
|
|
560
|
+
const cookieOptions = {
|
|
561
|
+
httpOnly: false,
|
|
562
|
+
// must be readable by client JS for the double-submit
|
|
563
|
+
sameSite: options?.cookie?.sameSite ?? "lax",
|
|
564
|
+
secure: options?.cookie?.secure ?? process.env.NODE_ENV !== "development",
|
|
565
|
+
path: options?.cookie?.path ?? "/",
|
|
566
|
+
maxAge: options?.cookie?.maxAge
|
|
567
|
+
};
|
|
568
|
+
return function csrfMiddleware(req, res, next) {
|
|
569
|
+
const existing = req.cookies?.[cookieName] || "";
|
|
570
|
+
let token = existing;
|
|
571
|
+
if (!isValidToken(existing, secret)) {
|
|
572
|
+
token = issueToken(secret);
|
|
573
|
+
res.cookie(cookieName, token, cookieOptions);
|
|
574
|
+
}
|
|
575
|
+
res.locals.csrfToken = token;
|
|
576
|
+
if (SAFE_METHODS.has(req.method)) return next();
|
|
577
|
+
if (ignorePaths.some((prefix) => req.path.startsWith(prefix))) return next();
|
|
578
|
+
const submitted = readSubmittedToken(req, headerName, fieldName);
|
|
579
|
+
if (isValidToken(existing, secret) && timingSafeEqual(submitted, existing)) {
|
|
580
|
+
return next();
|
|
581
|
+
}
|
|
582
|
+
res.status(403).json({ message: "Invalid CSRF token" });
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/express.ts
|
|
509
587
|
var methods = ["get", "post", "put", "patch", "delete"];
|
|
510
588
|
var isDev = process.env.NODE_ENV === "development";
|
|
589
|
+
function parseRoutePath(route) {
|
|
590
|
+
const slugRoute = route.replace(/[ ]/g, "");
|
|
591
|
+
const foundMethod = methods.find((method) => slugRoute.toUpperCase().endsWith(`.${method.toUpperCase()}`));
|
|
592
|
+
const templateRoute = foundMethod ? slugRoute.slice(0, -(foundMethod.length + 1)) : slugRoute;
|
|
593
|
+
let formattedRouteItem = templateRoute.replace(regex_default.spreadRoute, (_match, name) => ":" + name);
|
|
594
|
+
if (slugRoute.match(regex_default.isDynamicRoute)) {
|
|
595
|
+
for (const match of formattedRouteItem.matchAll(regex_default.dynamicRoutes)) {
|
|
596
|
+
formattedRouteItem = formattedRouteItem.replace(match[0], ":" + match[2]);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (formattedRouteItem.match(regex_default.catchAllRoute)) {
|
|
600
|
+
formattedRouteItem = formattedRouteItem.replace(regex_default.catchAllRoute, "/*");
|
|
601
|
+
}
|
|
602
|
+
return {
|
|
603
|
+
foundMethod,
|
|
604
|
+
formattedRouteItem,
|
|
605
|
+
templateRoute,
|
|
606
|
+
isSpread: regex_default.isSpreadRoute.test(templateRoute)
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
function spreadConcretePath(templateRoute, entry) {
|
|
610
|
+
return templateRoute.replace(regex_default.spreadRoute, (_match, name) => entry[name] ?? "").replace(regex_default.dynamicRoutes, (_match, _open, name) => entry[name] ?? "").replace(/\/{2,}/g, "/");
|
|
611
|
+
}
|
|
612
|
+
function spreadWildcardPath(templateRoute) {
|
|
613
|
+
return templateRoute.replace(regex_default.spreadRoute, "*").replace(regex_default.dynamicRoutes, (_match, _open, name) => ":" + name);
|
|
614
|
+
}
|
|
615
|
+
function spreadSlugName(templateRoute) {
|
|
616
|
+
const match = templateRoute.match(/\[\.\.\.(.*?)\]/);
|
|
617
|
+
return match ? match[1] : null;
|
|
618
|
+
}
|
|
619
|
+
async function resolveConfig(configFilePaths, componentImport) {
|
|
620
|
+
const baseConfig = configFilePaths ? await getCompleteConfig(configFilePaths, Date.now()) : {};
|
|
621
|
+
return componentImport?.config ? { ...baseConfig, ...componentImport.config } : baseConfig;
|
|
622
|
+
}
|
|
623
|
+
async function renderJSX(componentFn, config2, req, res, urlPath) {
|
|
624
|
+
const data = config2?.data ? await config2.data(req) : null;
|
|
625
|
+
const children = await componentFn({ req, res, data, config: config2, routePath: urlPath });
|
|
626
|
+
const output = config2?.wrapper ? await config2.wrapper({ req, children, config: config2, data, routePath: urlPath }) : children;
|
|
627
|
+
return doctypeHTML + output;
|
|
628
|
+
}
|
|
629
|
+
async function resolveSpreadRoute(templateRoute, config2, originalRoute) {
|
|
630
|
+
let entries = [];
|
|
631
|
+
if (typeof config2?.staticPaths === "function") {
|
|
632
|
+
try {
|
|
633
|
+
entries = await config2.staticPaths() || [];
|
|
634
|
+
} catch (err) {
|
|
635
|
+
console.error(err);
|
|
636
|
+
console.error("Could not resolve staticPaths for spread route", originalRoute);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
templateRoute,
|
|
641
|
+
entries,
|
|
642
|
+
validator: config2?.isValid || null
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
function registerSpreadRoutes(register, spread) {
|
|
646
|
+
const slugName = spreadSlugName(spread.templateRoute);
|
|
647
|
+
for (const entry of spread.entries) {
|
|
648
|
+
register(spreadConcretePath(spread.templateRoute, entry), [
|
|
649
|
+
(req, _res, next) => {
|
|
650
|
+
Object.assign(req.params, entry);
|
|
651
|
+
next();
|
|
652
|
+
}
|
|
653
|
+
]);
|
|
654
|
+
}
|
|
655
|
+
const { validator } = spread;
|
|
656
|
+
if (validator) {
|
|
657
|
+
register(spreadWildcardPath(spread.templateRoute), [
|
|
658
|
+
async (req, _res, next) => {
|
|
659
|
+
const slug = req.params[0];
|
|
660
|
+
if (slugName) req.params[slugName] = slug;
|
|
661
|
+
try {
|
|
662
|
+
if (await validator(slug, req)) {
|
|
663
|
+
next();
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
} catch (err) {
|
|
667
|
+
console.error(err);
|
|
668
|
+
}
|
|
669
|
+
next("route");
|
|
670
|
+
}
|
|
671
|
+
]);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
511
674
|
var OnInitEmitter = class extends EventEmitter {
|
|
512
675
|
};
|
|
513
676
|
var onInitEmitter = new OnInitEmitter();
|
|
@@ -532,33 +695,12 @@ function getAllBuiltRoutes() {
|
|
|
532
695
|
}
|
|
533
696
|
async function createRouteFunction(route, configFiles) {
|
|
534
697
|
const isJSX = route.originalRoute.endsWith(".tsx") || route.originalRoute.endsWith(".jsx");
|
|
535
|
-
const
|
|
536
|
-
const foundMethod = methods.find((method) => slugRoute.toUpperCase().endsWith(`.${method.toUpperCase()}`));
|
|
537
|
-
const isDynamicRoute = slugRoute.match(regex_default.isDynamicRoute);
|
|
538
|
-
let formattedRouteItem = slugRoute;
|
|
539
|
-
if (foundMethod) formattedRouteItem = formattedRouteItem.split(".").shift();
|
|
540
|
-
if (isDynamicRoute) {
|
|
541
|
-
const result = [...formattedRouteItem.matchAll(regex_default.dynamicRoutes)];
|
|
542
|
-
for (const match of result) {
|
|
543
|
-
formattedRouteItem = formattedRouteItem.replace(match[0], ":" + match[2]);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
const hasCatchAll = formattedRouteItem.match(regex_default.catchAllRoute);
|
|
547
|
-
if (hasCatchAll) formattedRouteItem = formattedRouteItem.replace(regex_default.catchAllRoute, "/*");
|
|
698
|
+
const { foundMethod, formattedRouteItem, templateRoute, isSpread } = parseRoutePath(route.route);
|
|
548
699
|
const componentImport = await import(route.path);
|
|
549
700
|
const componentFn = isDev ? null : componentImport?.default;
|
|
550
|
-
if (componentImport?.config?.onInit)
|
|
551
|
-
await componentImport.config?.onInit();
|
|
552
|
-
}
|
|
553
|
-
let config2 = {};
|
|
701
|
+
if (componentImport?.config?.onInit) await componentImport.config.onInit();
|
|
554
702
|
const configFilePaths = getConfigFiles(route.originalRoute, configFiles);
|
|
555
|
-
config2 =
|
|
556
|
-
if (componentImport?.config) {
|
|
557
|
-
config2 = {
|
|
558
|
-
...config2,
|
|
559
|
-
...componentImport.config
|
|
560
|
-
};
|
|
561
|
-
}
|
|
703
|
+
const config2 = await resolveConfig(configFilePaths, componentImport);
|
|
562
704
|
async function routeFn(req, res) {
|
|
563
705
|
const urlPath = req?.route?.path || "/";
|
|
564
706
|
try {
|
|
@@ -568,47 +710,37 @@ async function createRouteFunction(route, configFiles) {
|
|
|
568
710
|
return;
|
|
569
711
|
}
|
|
570
712
|
if (componentFn && !isDev) {
|
|
571
|
-
if (isJSX)
|
|
572
|
-
|
|
573
|
-
const originalResult = await componentFn({ req, res, data, routePath: urlPath });
|
|
574
|
-
const output = config2?.wrapper ? await config2.wrapper({ req, children: originalResult, config: config2, data, routePath: urlPath }) : originalResult;
|
|
575
|
-
res.send(doctypeHTML + output);
|
|
576
|
-
} else {
|
|
577
|
-
await componentFn(req, res);
|
|
578
|
-
}
|
|
713
|
+
if (isJSX) res.send(await renderJSX(componentFn, config2, req, res, urlPath));
|
|
714
|
+
else await componentFn(req, res);
|
|
579
715
|
return;
|
|
580
716
|
}
|
|
581
717
|
await triggerXPineOnLoad(true);
|
|
582
|
-
const
|
|
583
|
-
const componentFnDev = componentImportDev.default;
|
|
718
|
+
const devImport = await import(route.path + `?cache=${Date.now()}`);
|
|
584
719
|
onInitEmitter.emit("triggerOnInit", getAllBuiltRoutes());
|
|
585
720
|
if (isJSX) {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
config3 = {
|
|
589
|
-
...config3,
|
|
590
|
-
...componentImportDev.config
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
const data = config3?.data ? await config3.data(req) : null;
|
|
594
|
-
const originalResult = await componentFnDev({ req, res, data, config: config3, routePath: urlPath });
|
|
595
|
-
const output = config3?.wrapper ? await config3.wrapper({ req, children: originalResult, config: config3, data, routePath: urlPath }) : originalResult;
|
|
721
|
+
const devConfig = await resolveConfig(configFilePaths, devImport);
|
|
722
|
+
const html2 = await renderJSX(devImport.default, devConfig, req, res, urlPath);
|
|
596
723
|
context.clear();
|
|
597
|
-
res.send(
|
|
724
|
+
res.send(html2);
|
|
598
725
|
} else {
|
|
599
|
-
await
|
|
726
|
+
await devImport.default(req, res);
|
|
600
727
|
}
|
|
601
728
|
} catch (err) {
|
|
602
729
|
console.error(err);
|
|
603
|
-
|
|
730
|
+
const status = err?.status || 500;
|
|
731
|
+
const message = isDev || err?.status ? err?.message || "Error" : "Internal Server Error";
|
|
732
|
+
res.status(status).send(message);
|
|
604
733
|
}
|
|
605
734
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
735
|
+
const spread = isSpread ? await resolveSpreadRoute(templateRoute, config2, route.originalRoute) : null;
|
|
736
|
+
return { formattedRouteItem, foundMethod, route, config: config2, routeFn, spread };
|
|
737
|
+
}
|
|
738
|
+
function buildRouteRegister(router, method, config2, routeFn) {
|
|
739
|
+
return (routePath, preHandlers = []) => {
|
|
740
|
+
const handlers = [...preHandlers];
|
|
741
|
+
if (config2?.routeMiddleware) handlers.push(config2.routeMiddleware);
|
|
742
|
+
handlers.push(routeFn);
|
|
743
|
+
router[method](routePath, ...handlers);
|
|
612
744
|
};
|
|
613
745
|
}
|
|
614
746
|
async function createRouter() {
|
|
@@ -619,46 +751,50 @@ async function createRouter() {
|
|
|
619
751
|
await triggerXPineOnLoad();
|
|
620
752
|
for (const route of routeMap) {
|
|
621
753
|
try {
|
|
622
|
-
const { formattedRouteItem, foundMethod, config:
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
}
|
|
628
|
-
routeResults.push({
|
|
629
|
-
formattedRouteItem,
|
|
630
|
-
foundMethod,
|
|
631
|
-
route,
|
|
632
|
-
routeFn
|
|
633
|
-
});
|
|
754
|
+
const { formattedRouteItem, foundMethod, config: routeConfig, routeFn, spread } = await createRouteFunction(route, configFiles);
|
|
755
|
+
const register = buildRouteRegister(router, foundMethod || "get", routeConfig, routeFn);
|
|
756
|
+
if (spread) registerSpreadRoutes(register, spread);
|
|
757
|
+
else register(formattedRouteItem);
|
|
758
|
+
routeResults.push({ formattedRouteItem, foundMethod, route, routeFn });
|
|
634
759
|
} catch (err) {
|
|
635
760
|
console.error(err);
|
|
636
761
|
}
|
|
637
762
|
}
|
|
638
|
-
return {
|
|
639
|
-
router,
|
|
640
|
-
routeResults
|
|
641
|
-
};
|
|
763
|
+
return { router, routeResults };
|
|
642
764
|
}
|
|
643
765
|
async function verifyUserMiddleware(req, _res, next) {
|
|
644
|
-
const
|
|
766
|
+
const usertoken = req.cookies?.usertoken;
|
|
645
767
|
if (!usertoken) {
|
|
646
768
|
req.user = null;
|
|
769
|
+
next();
|
|
770
|
+
return;
|
|
647
771
|
}
|
|
648
772
|
try {
|
|
649
773
|
const { user } = await verifyUser(usertoken);
|
|
650
774
|
req.user = user;
|
|
651
|
-
} catch
|
|
775
|
+
} catch {
|
|
652
776
|
req.user = null;
|
|
653
777
|
}
|
|
654
778
|
next();
|
|
655
779
|
}
|
|
656
780
|
async function verifyAuthenticatedBundleMiddleware(req, res, next) {
|
|
657
|
-
|
|
781
|
+
let decodedPath;
|
|
782
|
+
try {
|
|
783
|
+
decodedPath = decodeURIComponent(req?.path || "");
|
|
784
|
+
} catch {
|
|
785
|
+
next();
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
if (!decodedPath.startsWith("/scripts/")) {
|
|
789
|
+
next();
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
const fileName = decodedPath.split("/").pop() || "";
|
|
793
|
+
if (!fileName.endsWith(".js")) {
|
|
658
794
|
next();
|
|
659
795
|
return;
|
|
660
796
|
}
|
|
661
|
-
const bundle =
|
|
797
|
+
const bundle = fileName.slice(0, -".js".length);
|
|
662
798
|
const foundBundle = config?.bundles?.find((bundleItem) => bundleItem?.id === bundle);
|
|
663
799
|
if (!foundBundle) {
|
|
664
800
|
next();
|
|
@@ -677,6 +813,9 @@ async function createXPineRouter(app, beforeErrorRoute) {
|
|
|
677
813
|
app.use(verifyAuthenticatedBundleMiddleware);
|
|
678
814
|
app.use(express.static(config.distPublicDir));
|
|
679
815
|
app.use(requestIP.mw());
|
|
816
|
+
if (config?.csrf) {
|
|
817
|
+
app.use(createCsrfMiddleware(typeof config.csrf === "object" ? config.csrf : void 0));
|
|
818
|
+
}
|
|
680
819
|
const { router, routeResults } = await createRouter();
|
|
681
820
|
app.use(function replaceableRouter(req, res, next) {
|
|
682
821
|
router(req, res, next);
|
|
@@ -696,11 +835,13 @@ function routeHasStaticPath(route, params) {
|
|
|
696
835
|
const paramEntries = Object.entries(params);
|
|
697
836
|
let routeToStaticPath = route;
|
|
698
837
|
for (const [key, value] of paramEntries) {
|
|
699
|
-
routeToStaticPath = routeToStaticPath.replace(`:${key}`, value);
|
|
700
|
-
if (key === "0") routeToStaticPath = routeToStaticPath.replace(/\/\*/g, `/${value}`);
|
|
838
|
+
routeToStaticPath = routeToStaticPath.replace(`:${key}`, () => value);
|
|
839
|
+
if (key === "0") routeToStaticPath = routeToStaticPath.replace(/\/\*/g, () => `/${value}`);
|
|
701
840
|
}
|
|
702
841
|
routeToStaticPath += "/index.html";
|
|
703
|
-
const
|
|
842
|
+
const pagesDir = path5.resolve(config.distPagesDir);
|
|
843
|
+
const outputPath = path5.resolve(pagesDir, "." + path5.posix.normalize("/" + routeToStaticPath));
|
|
844
|
+
if (outputPath !== pagesDir && !outputPath.startsWith(pagesDir + path5.sep)) return false;
|
|
704
845
|
if (fs5.existsSync(outputPath)) return outputPath;
|
|
705
846
|
return false;
|
|
706
847
|
}
|
|
@@ -1118,7 +1259,7 @@ async function buildStaticFiles(config2, component, componentImport, builtCompon
|
|
|
1118
1259
|
function determineOutputPathForStaticPath(builtComponentPath, componentDynamicPaths) {
|
|
1119
1260
|
if (componentDynamicPaths?.length) {
|
|
1120
1261
|
return componentDynamicPaths.reduce((total, current) => {
|
|
1121
|
-
return total.replace(`/[${current}]`, "");
|
|
1262
|
+
return total.replace(`/[...${current}]`, "").replace(`/[${current}]`, "");
|
|
1122
1263
|
}, path6.dirname(builtComponentPath));
|
|
1123
1264
|
} else {
|
|
1124
1265
|
if (builtComponentPath?.match(regex_default.endsWithIndex)) {
|
|
@@ -1134,7 +1275,8 @@ function getComponentDynamicPaths(componentPath) {
|
|
|
1134
1275
|
if (!matches?.length) return null;
|
|
1135
1276
|
const output = [];
|
|
1136
1277
|
for (const match of matches) {
|
|
1137
|
-
|
|
1278
|
+
if (!match[2]) continue;
|
|
1279
|
+
output.push(match[2].replace(/^\.\.\./, ""));
|
|
1138
1280
|
}
|
|
1139
1281
|
return output;
|
|
1140
1282
|
}
|
|
@@ -1316,9 +1458,9 @@ async function runDevServer() {
|
|
|
1316
1458
|
});
|
|
1317
1459
|
}
|
|
1318
1460
|
function asyncServerClose(server) {
|
|
1319
|
-
return new Promise((resolve
|
|
1320
|
-
server.close();
|
|
1321
|
-
|
|
1461
|
+
return new Promise((resolve) => {
|
|
1462
|
+
server.close(() => resolve(true));
|
|
1463
|
+
server.closeAllConnections?.();
|
|
1322
1464
|
});
|
|
1323
1465
|
}
|
|
1324
1466
|
function createRebuildEmitter(delay = 500) {
|
|
@@ -1326,39 +1468,51 @@ function createRebuildEmitter(delay = 500) {
|
|
|
1326
1468
|
}
|
|
1327
1469
|
;
|
|
1328
1470
|
const rebuildEmitter = new RebuildEmitter();
|
|
1329
|
-
let
|
|
1330
|
-
let hasEmitted = false;
|
|
1471
|
+
let timeout = null;
|
|
1331
1472
|
rebuildEmitter.on("rebuild-server", () => {
|
|
1332
|
-
|
|
1333
|
-
|
|
1473
|
+
if (timeout) clearTimeout(timeout);
|
|
1474
|
+
timeout = setTimeout(() => {
|
|
1475
|
+
timeout = null;
|
|
1476
|
+
rebuildEmitter.emit("done");
|
|
1477
|
+
}, delay);
|
|
1334
1478
|
});
|
|
1335
|
-
function trigger() {
|
|
1336
|
-
start = Date.now();
|
|
1337
|
-
const interval = setInterval(() => {
|
|
1338
|
-
if (hasEmitted) return;
|
|
1339
|
-
const now = Date.now();
|
|
1340
|
-
if (now - start >= delay) {
|
|
1341
|
-
rebuildEmitter.emit("done");
|
|
1342
|
-
clearInterval(interval);
|
|
1343
|
-
hasEmitted = true;
|
|
1344
|
-
}
|
|
1345
|
-
}, 100);
|
|
1346
|
-
}
|
|
1347
1479
|
return rebuildEmitter;
|
|
1348
1480
|
}
|
|
1349
1481
|
|
|
1350
1482
|
// src/util/html.ts
|
|
1483
|
+
var RawHtml = class {
|
|
1484
|
+
constructor(value) {
|
|
1485
|
+
this.value = value;
|
|
1486
|
+
}
|
|
1487
|
+
toString() {
|
|
1488
|
+
return this.value;
|
|
1489
|
+
}
|
|
1490
|
+
};
|
|
1491
|
+
var RAW_TEXT_ELEMENTS = /* @__PURE__ */ new Set(["script", "style"]);
|
|
1492
|
+
function escapeHtml(value) {
|
|
1493
|
+
return String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1494
|
+
}
|
|
1495
|
+
function raw(value) {
|
|
1496
|
+
return new RawHtml(value);
|
|
1497
|
+
}
|
|
1498
|
+
function renderChild(child, rawText) {
|
|
1499
|
+
if (Array.isArray(child)) return child.map((item) => renderChild(item, rawText)).join("");
|
|
1500
|
+
if (child instanceof RawHtml) return child.value;
|
|
1501
|
+
if (!child) return "";
|
|
1502
|
+
return rawText ? String(child) : escapeHtml(child);
|
|
1503
|
+
}
|
|
1351
1504
|
var html = class {
|
|
1505
|
+
static raw = raw;
|
|
1352
1506
|
static attributeObjectToString(props) {
|
|
1353
1507
|
if (!props) return "";
|
|
1354
1508
|
return Object.entries(props).filter(([, value]) => value !== null && value !== void 0).map(([key, value], index) => {
|
|
1355
1509
|
const start = index === 0 ? " " : "";
|
|
1356
|
-
return `${start}${key}="${value}"`;
|
|
1510
|
+
return `${start}${key}="${escapeHtml(value)}"`;
|
|
1357
1511
|
}).join(" ");
|
|
1358
1512
|
}
|
|
1359
1513
|
static async fragment(props) {
|
|
1360
1514
|
const childrenResult = await Promise.all(props.children.flat());
|
|
1361
|
-
return childrenResult.
|
|
1515
|
+
return new RawHtml(childrenResult.map((child) => renderChild(child, false)).join(""));
|
|
1362
1516
|
}
|
|
1363
1517
|
static async createElement(type, props, ...children) {
|
|
1364
1518
|
const childrenResult = await Promise.all(children.flat());
|
|
@@ -1366,7 +1520,9 @@ var html = class {
|
|
|
1366
1520
|
const result = await type({ ...props, children: childrenResult });
|
|
1367
1521
|
return result;
|
|
1368
1522
|
}
|
|
1369
|
-
|
|
1523
|
+
const rawText = RAW_TEXT_ELEMENTS.has(type);
|
|
1524
|
+
const inner = childrenResult.map((child) => renderChild(child, rawText)).join("");
|
|
1525
|
+
return new RawHtml(`<${type}${this.attributeObjectToString(props)}>${inner}</${type}>`);
|
|
1370
1526
|
}
|
|
1371
1527
|
};
|
|
1372
1528
|
function JSXRuntime() {
|
|
@@ -1379,6 +1535,7 @@ function getCurrentBreakpoint() {
|
|
|
1379
1535
|
}
|
|
1380
1536
|
export {
|
|
1381
1537
|
JSXRuntime,
|
|
1538
|
+
RawHtml,
|
|
1382
1539
|
addToContextArray,
|
|
1383
1540
|
buildApp,
|
|
1384
1541
|
buildFilesWithConfigs,
|
|
@@ -1389,9 +1546,11 @@ export {
|
|
|
1389
1546
|
config,
|
|
1390
1547
|
context,
|
|
1391
1548
|
createContext,
|
|
1549
|
+
createCsrfMiddleware,
|
|
1392
1550
|
createRouter,
|
|
1393
1551
|
createXPineRouter,
|
|
1394
1552
|
determineOutputPathForStaticPath,
|
|
1553
|
+
escapeHtml,
|
|
1395
1554
|
filePathToURLPath,
|
|
1396
1555
|
fromRoot,
|
|
1397
1556
|
getComponentDynamicPaths,
|
|
@@ -1399,9 +1558,13 @@ export {
|
|
|
1399
1558
|
getTokenFromRequest,
|
|
1400
1559
|
html,
|
|
1401
1560
|
logSize,
|
|
1561
|
+
raw,
|
|
1402
1562
|
routeHasStaticPath,
|
|
1403
1563
|
runDevServer,
|
|
1404
1564
|
setupEnv,
|
|
1405
1565
|
signUser,
|
|
1566
|
+
spreadConcretePath,
|
|
1567
|
+
spreadSlugName,
|
|
1568
|
+
spreadWildcardPath,
|
|
1406
1569
|
verifyUser
|
|
1407
1570
|
};
|
package/dist/src/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAgBzC,wBAAsB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,oBAgB7D;AAED,wBAAsB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAO5D;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,aAAa,UAKrD"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { NextFunction, Request, Response } from 'express';
|
|
2
|
+
export type CsrfOptions = {
|
|
3
|
+
cookieName?: string;
|
|
4
|
+
headerName?: string;
|
|
5
|
+
fieldName?: string;
|
|
6
|
+
secret?: string;
|
|
7
|
+
cookie?: {
|
|
8
|
+
sameSite?: boolean | 'lax' | 'strict' | 'none';
|
|
9
|
+
secure?: boolean;
|
|
10
|
+
path?: string;
|
|
11
|
+
maxAge?: number;
|
|
12
|
+
};
|
|
13
|
+
ignorePaths?: string[];
|
|
14
|
+
};
|
|
15
|
+
export declare function createCsrfMiddleware(options?: CsrfOptions): (req: Request, res: Response, next: NextFunction) => void;
|
|
16
|
+
//# sourceMappingURL=csrf.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csrf.d.ts","sourceRoot":"","sources":["../../src/csrf.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAK1D,MAAM,MAAM,WAAW,GAAG;IAExB,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,MAAM,CAAC,EAAE;QACP,QAAQ,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;QAC/C,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;IAEF,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;AAoDF,wBAAgB,oBAAoB,CAAC,OAAO,CAAC,EAAE,WAAW,SAepB,OAAO,OAAO,QAAQ,QAAQ,YAAY,UAqB/E"}
|
package/dist/src/express.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { Express } from 'express';
|
|
2
|
+
export declare function spreadConcretePath(templateRoute: string, entry: {
|
|
3
|
+
[key: string]: string;
|
|
4
|
+
}): string;
|
|
5
|
+
export declare function spreadWildcardPath(templateRoute: string): string;
|
|
6
|
+
export declare function spreadSlugName(templateRoute: string): string | null;
|
|
2
7
|
export declare function createRouter(): Promise<{
|
|
3
8
|
router: import("express-serve-static-core").Router;
|
|
4
9
|
routeResults: any[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../../src/express.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAgB,OAAO,
|
|
1
|
+
{"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../../src/express.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAgB,OAAO,EAA6C,MAAM,SAAS,CAAC;AA8FpG,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,GAAG,MAAM,CAKlG;AAGD,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAIhE;AAGD,wBAAgB,cAAc,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGnE;AAqLD,wBAAsB,YAAY;;;GAuBjC;AAuDD,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,GAAG,EAAE,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,iBAgC1F;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,GAAG,MAAM,GAAG,KAAK,CAoBnG;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAE,OAAc,UAQzE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/scripts/build.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAE,UAAU,EAA2B,aAAa,EAAE,MAAM,aAAa,CAAC;AAajF,KAAK,YAAY,GAAG;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAA;AAED,wBAAsB,QAAQ,CAAC,IAAI,EAAE,YAAY,iBAgDhD;AA4JD,wBAAsB,yBAAyB,kBAiC9C;AAED,wBAAsB,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,eAAe,WAAkB,iBAa9F;AAED,wBAAsB,qBAAqB,CAAC,aAAa,EAAE,aAAa,EAAE,iBAezE;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,eAAe,EAAE,GAAG,EAAE,kBAAkB,EAAE,MAAM,iBA2FpI;AAED,wBAAgB,gCAAgC,CAAC,kBAAkB,EAAE,MAAM,EAAE,qBAAqB,CAAC,EAAE,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/scripts/build.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAE,UAAU,EAA2B,aAAa,EAAE,MAAM,aAAa,CAAC;AAajF,KAAK,YAAY,GAAG;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAA;AAED,wBAAsB,QAAQ,CAAC,IAAI,EAAE,YAAY,iBAgDhD;AA4JD,wBAAsB,yBAAyB,kBAiC9C;AAED,wBAAsB,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,eAAe,WAAkB,iBAa9F;AAED,wBAAsB,qBAAqB,CAAC,aAAa,EAAE,aAAa,EAAE,iBAezE;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,eAAe,EAAE,GAAG,EAAE,kBAAkB,EAAE,MAAM,iBA2FpI;AAED,wBAAgB,gCAAgC,CAAC,kBAAkB,EAAE,MAAM,EAAE,qBAAqB,CAAC,EAAE,MAAM,EAAE,UAgB5G;AAED,wBAAgB,wBAAwB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,EAAE,CAUxE;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;CACZ,CAAA;AAGD,wBAAsB,eAAe,CAAC,aAAa,EAAE,aAAa,EAAE,EAAE,KAAK,CAAC,EAAE,OAAO,iBAoDpF;AAED,wBAAsB,YAAY,kBA4CjC"}
|
|
@@ -321,6 +321,9 @@ var regex_default = {
|
|
|
321
321
|
configFile: /\+config\.[tj]sx?/g,
|
|
322
322
|
dynamicRoutes: /(\[)(.*?)(\])/g,
|
|
323
323
|
isDynamicRoute: /\[(.*)\]/g,
|
|
324
|
+
// Multi-segment (slash-containing) dynamic route, e.g. [...slug]
|
|
325
|
+
spreadRoute: /\[\.\.\.(.*?)\]/g,
|
|
326
|
+
isSpreadRoute: /\[\.\.\..*?\]/,
|
|
324
327
|
endsWithTSX: /\.tsx$/,
|
|
325
328
|
endsWithJSX: /\.jsx$/,
|
|
326
329
|
endsWithJs: /\.js$/,
|
|
@@ -891,7 +894,7 @@ async function buildStaticFiles(config2, component, componentImport, builtCompon
|
|
|
891
894
|
function determineOutputPathForStaticPath(builtComponentPath, componentDynamicPaths) {
|
|
892
895
|
if (componentDynamicPaths?.length) {
|
|
893
896
|
return componentDynamicPaths.reduce((total, current) => {
|
|
894
|
-
return total.replace(`/[${current}]`, "");
|
|
897
|
+
return total.replace(`/[...${current}]`, "").replace(`/[${current}]`, "");
|
|
895
898
|
}, path5.dirname(builtComponentPath));
|
|
896
899
|
} else {
|
|
897
900
|
if (builtComponentPath?.match(regex_default.endsWithIndex)) {
|
|
@@ -907,7 +910,8 @@ function getComponentDynamicPaths(componentPath) {
|
|
|
907
910
|
if (!matches?.length) return null;
|
|
908
911
|
const output = [];
|
|
909
912
|
for (const match of matches) {
|
|
910
|
-
|
|
913
|
+
if (!match[2]) continue;
|
|
914
|
+
output.push(match[2].replace(/^\.\.\./, ""));
|
|
911
915
|
}
|
|
912
916
|
return output;
|
|
913
917
|
}
|
|
@@ -328,6 +328,9 @@ var regex_default = {
|
|
|
328
328
|
configFile: /\+config\.[tj]sx?/g,
|
|
329
329
|
dynamicRoutes: /(\[)(.*?)(\])/g,
|
|
330
330
|
isDynamicRoute: /\[(.*)\]/g,
|
|
331
|
+
// Multi-segment (slash-containing) dynamic route, e.g. [...slug]
|
|
332
|
+
spreadRoute: /\[\.\.\.(.*?)\]/g,
|
|
333
|
+
isSpreadRoute: /\[\.\.\..*?\]/,
|
|
331
334
|
endsWithTSX: /\.tsx$/,
|
|
332
335
|
endsWithJSX: /\.jsx$/,
|
|
333
336
|
endsWithJs: /\.js$/,
|
|
@@ -898,7 +901,7 @@ async function buildStaticFiles(config2, component, componentImport, builtCompon
|
|
|
898
901
|
function determineOutputPathForStaticPath(builtComponentPath, componentDynamicPaths) {
|
|
899
902
|
if (componentDynamicPaths?.length) {
|
|
900
903
|
return componentDynamicPaths.reduce((total, current) => {
|
|
901
|
-
return total.replace(`/[${current}]`, "");
|
|
904
|
+
return total.replace(`/[...${current}]`, "").replace(`/[${current}]`, "");
|
|
902
905
|
}, path5.dirname(builtComponentPath));
|
|
903
906
|
} else {
|
|
904
907
|
if (builtComponentPath?.match(regex_default.endsWithIndex)) {
|
|
@@ -914,7 +917,8 @@ function getComponentDynamicPaths(componentPath) {
|
|
|
914
917
|
if (!matches?.length) return null;
|
|
915
918
|
const output = [];
|
|
916
919
|
for (const match of matches) {
|
|
917
|
-
|
|
920
|
+
if (!match[2]) continue;
|
|
921
|
+
output.push(match[2].replace(/^\.\.\./, ""));
|
|
918
922
|
}
|
|
919
923
|
return output;
|
|
920
924
|
}
|
|
@@ -1096,9 +1100,9 @@ async function runDevServer() {
|
|
|
1096
1100
|
});
|
|
1097
1101
|
}
|
|
1098
1102
|
function asyncServerClose(server) {
|
|
1099
|
-
return new Promise((resolve
|
|
1100
|
-
server.close();
|
|
1101
|
-
|
|
1103
|
+
return new Promise((resolve) => {
|
|
1104
|
+
server.close(() => resolve(true));
|
|
1105
|
+
server.closeAllConnections?.();
|
|
1102
1106
|
});
|
|
1103
1107
|
}
|
|
1104
1108
|
function createRebuildEmitter(delay = 500) {
|
|
@@ -1106,24 +1110,14 @@ function createRebuildEmitter(delay = 500) {
|
|
|
1106
1110
|
}
|
|
1107
1111
|
;
|
|
1108
1112
|
const rebuildEmitter = new RebuildEmitter();
|
|
1109
|
-
let
|
|
1110
|
-
let hasEmitted = false;
|
|
1113
|
+
let timeout = null;
|
|
1111
1114
|
rebuildEmitter.on("rebuild-server", () => {
|
|
1112
|
-
|
|
1113
|
-
|
|
1115
|
+
if (timeout) clearTimeout(timeout);
|
|
1116
|
+
timeout = setTimeout(() => {
|
|
1117
|
+
timeout = null;
|
|
1118
|
+
rebuildEmitter.emit("done");
|
|
1119
|
+
}, delay);
|
|
1114
1120
|
});
|
|
1115
|
-
function trigger() {
|
|
1116
|
-
start = Date.now();
|
|
1117
|
-
const interval = setInterval(() => {
|
|
1118
|
-
if (hasEmitted) return;
|
|
1119
|
-
const now = Date.now();
|
|
1120
|
-
if (now - start >= delay) {
|
|
1121
|
-
rebuildEmitter.emit("done");
|
|
1122
|
-
clearInterval(interval);
|
|
1123
|
-
hasEmitted = true;
|
|
1124
|
-
}
|
|
1125
|
-
}, 100);
|
|
1126
|
-
}
|
|
1127
1121
|
return rebuildEmitter;
|
|
1128
1122
|
}
|
|
1129
1123
|
|
package/dist/src/util/html.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
export declare class RawHtml {
|
|
2
|
+
value: string;
|
|
3
|
+
constructor(value: string);
|
|
4
|
+
toString(): string;
|
|
5
|
+
}
|
|
6
|
+
export declare function escapeHtml(value: unknown): string;
|
|
7
|
+
export declare function raw(value: string): RawHtml;
|
|
1
8
|
export declare class html {
|
|
9
|
+
static raw: typeof raw;
|
|
2
10
|
static attributeObjectToString(props: any): string;
|
|
3
|
-
static fragment(props: any): Promise<
|
|
11
|
+
static fragment(props: any): Promise<RawHtml>;
|
|
4
12
|
static createElement(type: any, props: any, ...children: any[]): Promise<any>;
|
|
5
13
|
}
|
|
6
14
|
export declare function JSXRuntime(): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../../src/util/html.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../../src/util/html.ts"],"names":[],"mappings":"AAGA,qBAAa,OAAO;IACC,KAAK,EAAE,MAAM;gBAAb,KAAK,EAAE,MAAM;IAChC,QAAQ;CACT;AAKD,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAOjD;AAID,wBAAgB,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE1C;AAUD,qBAAa,IAAI;IACf,MAAM,CAAC,GAAG,aAAO;IAEjB,MAAM,CAAC,uBAAuB,CAAC,KAAK,KAAA;WAYvB,QAAQ,CAAC,KAAK,KAAA;WAKd,aAAa,CAAC,IAAI,KAAA,EAAE,KAAK,KAAA,EAAE,GAAG,QAAQ,OAAA;CAWpD;AAED,wBAAgB,UAAU,YAEzB"}
|
package/dist/src/util/regex.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"regex.d.ts","sourceRoot":"","sources":["../../../src/util/regex.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"regex.d.ts","sourceRoot":"","sources":["../../../src/util/regex.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA,wBAoBE"}
|
package/dist/types.d.ts
CHANGED
|
@@ -21,10 +21,13 @@ export type WrapperProps = {
|
|
|
21
21
|
export type ConfigFile = {
|
|
22
22
|
staticPaths?: boolean | (() => Promise<{
|
|
23
23
|
[key: string]: string;
|
|
24
|
-
}[]>
|
|
24
|
+
}[]> | {
|
|
25
|
+
[key: string]: string;
|
|
26
|
+
}[]);
|
|
25
27
|
wrapper?: (props: WrapperProps) => Promise<any>;
|
|
26
28
|
data?: (req: ServerRequest) => Promise<any>;
|
|
27
29
|
routeMiddleware?: (req: ServerRequest, res: Response, next: NextFunction) => void;
|
|
30
|
+
isValid?: (slug: string, req: ServerRequest) => boolean | Promise<boolean>;
|
|
28
31
|
};
|
|
29
32
|
export type PageProps = {
|
|
30
33
|
req: ServerRequest;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,MAAM,MAAM,WAAW,GAAG;IACxB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB,CAAA;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAA;AAED,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG;IACpC,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,EAAE,aAAa,CAAC;IACnB,QAAQ,EAAE,GAAG,CAAC;IACd,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,WAAW,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,EAAE,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,MAAM,MAAM,WAAW,GAAG;IACxB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB,CAAA;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAA;AAED,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG;IACpC,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,EAAE,aAAa,CAAC;IACnB,QAAQ,EAAE,GAAG,CAAC;IACd,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,WAAW,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,EAAE,CAAC,GAAG;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,EAAE,CAAC,CAAC;IACnG,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;IAChD,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;IAC5C,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,aAAa,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IAIlF,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC5E,CAAA;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,GAAG,EAAE,aAAa,CAAC;IACnB,GAAG,EAAE,QAAQ,CAAC;IACd,IAAI,EAAE,GAAG,CAAC;IACV,SAAS,EAAE,MAAM,CAAC;CACnB,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,MAAM,EAAE,EAAE,CAAC,UAAU,CAAC;CACvB,CAAA"}
|
package/package.json
CHANGED
package/types.ts
CHANGED
|
@@ -24,10 +24,14 @@ export type WrapperProps = {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export type ConfigFile = {
|
|
27
|
-
staticPaths?: boolean | (() => Promise<{ [key: string]: string }[]>);
|
|
27
|
+
staticPaths?: boolean | (() => Promise<{ [key: string]: string }[]> | { [key: string]: string }[]);
|
|
28
28
|
wrapper?: (props: WrapperProps) => Promise<any>;
|
|
29
29
|
data?: (req: ServerRequest) => Promise<any>;
|
|
30
30
|
routeMiddleware?: (req: ServerRequest, res: Response, next: NextFunction) => void;
|
|
31
|
+
// For multi-segment dynamic routes ([...slug]): validate a slug that was not
|
|
32
|
+
// generated at build time so it can be resolved safely at request time.
|
|
33
|
+
// Return false (the default for unknown slugs) to fall through to the 404 handler.
|
|
34
|
+
isValid?: (slug: string, req: ServerRequest) => boolean | Promise<boolean>;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
export type PageProps = {
|