arcway 0.4.17 → 0.4.19
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/package.json +1 -1
- package/server/config/modules/database.js +7 -0
- package/server/config/modules/mail.js +7 -0
- package/server/config/modules/pages.js +75 -3
- package/server/config/modules/session.js +3 -2
- package/server/config/port.js +10 -0
- package/server/pages/handler.js +105 -13
- package/server/pages/vite-dev.js +2 -2
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { normalizePort } from '../port.js';
|
|
2
3
|
|
|
3
4
|
const DEFAULTS = {
|
|
4
5
|
sqliteFilename: '.build/db/arcway.db',
|
|
@@ -15,6 +16,12 @@ function resolve(config, { rootDir } = {}) {
|
|
|
15
16
|
const db = { ...DEFAULTS, ...config.database };
|
|
16
17
|
// Resolve friendly client names to actual knex driver names
|
|
17
18
|
db.client = CLIENT_MAP[db.client] ?? db.client;
|
|
19
|
+
if (db.connection && typeof db.connection === 'object' && db.connection.port !== undefined) {
|
|
20
|
+
db.connection = {
|
|
21
|
+
...db.connection,
|
|
22
|
+
port: normalizePort(db.connection.port, 'database.connection.port'),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
18
25
|
if (db.dir && !path.isAbsolute(db.dir)) {
|
|
19
26
|
db.dir = path.resolve(rootDir, db.dir);
|
|
20
27
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeDurationDays, normalizeDurationSeconds } from '../duration.js';
|
|
2
|
+
import { normalizePort } from '../port.js';
|
|
2
3
|
|
|
3
4
|
const DEFAULTS = {
|
|
4
5
|
driver: 'console',
|
|
@@ -8,6 +9,12 @@ const DEFAULTS = {
|
|
|
8
9
|
function resolve(config) {
|
|
9
10
|
if (!config.mail) return config;
|
|
10
11
|
const mail = { ...DEFAULTS, ...config.mail };
|
|
12
|
+
if (mail.smtp?.port !== undefined) {
|
|
13
|
+
mail.smtp = {
|
|
14
|
+
...mail.smtp,
|
|
15
|
+
port: normalizePort(mail.smtp.port, 'mail.smtp.port'),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
11
18
|
if (mail.inbound?.imap?.pollIntervalSeconds !== undefined) {
|
|
12
19
|
mail.inbound.imap.pollIntervalSeconds = normalizeDurationSeconds(
|
|
13
20
|
mail.inbound.imap.pollIntervalSeconds,
|
|
@@ -12,6 +12,77 @@ const DEFAULTS = {
|
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
const HMR_KEYS = new Set(['protocol', 'host', 'clientPort']);
|
|
15
|
+
const REWRITE_TOKEN = /:([A-Za-z][A-Za-z0-9]*)(\*)?/g;
|
|
16
|
+
|
|
17
|
+
function rewritePrefix(destination) {
|
|
18
|
+
const tokenIndex = destination.indexOf(':');
|
|
19
|
+
return destination.slice(0, tokenIndex < 0 ? destination.length : tokenIndex).replace(/\/$/, '');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function validRewritePath(value) {
|
|
23
|
+
return typeof value === 'string' && value.startsWith('/') && !/[?#\s]/.test(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function validateRewriteRules(rules) {
|
|
27
|
+
if (rules.length === 0) throw new TypeError('Invalid config: pages.rewrite must not be empty');
|
|
28
|
+
for (const rule of rules) {
|
|
29
|
+
if (!rule || typeof rule !== 'object' || Array.isArray(rule)) {
|
|
30
|
+
throw new TypeError('Invalid config: each pages.rewrite rule must be an object');
|
|
31
|
+
}
|
|
32
|
+
if (!validRewritePath(rule.source) || !validRewritePath(rule.destination)) {
|
|
33
|
+
throw new TypeError(
|
|
34
|
+
'Invalid config: each pages.rewrite rule requires absolute source and destination paths',
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const sourceSegments = rule.source.split('/').slice(1);
|
|
38
|
+
for (const [index, segment] of sourceSegments.entries()) {
|
|
39
|
+
if (segment.includes(':') && !/^:[A-Za-z][A-Za-z0-9]*\*?$/.test(segment)) {
|
|
40
|
+
throw new TypeError(
|
|
41
|
+
'Invalid config: pages.rewrite source tokens must occupy a path segment',
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (segment.endsWith('*') && index !== sourceSegments.length - 1) {
|
|
45
|
+
throw new TypeError('Invalid config: a pages.rewrite catch-all token must be last');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const sourceTokens = new Set([...rule.source.matchAll(REWRITE_TOKEN)].map((match) => match[1]));
|
|
49
|
+
for (const match of rule.destination.matchAll(REWRITE_TOKEN)) {
|
|
50
|
+
if (match[1] !== 'host' && !sourceTokens.has(match[1])) {
|
|
51
|
+
throw new TypeError(`Invalid config: unknown pages.rewrite token :${match[1]}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (rule.destination.replace(REWRITE_TOKEN, '').includes(':')) {
|
|
55
|
+
throw new TypeError('Invalid config: pages.rewrite destination contains an invalid token');
|
|
56
|
+
}
|
|
57
|
+
const prefix = rewritePrefix(rule.destination);
|
|
58
|
+
if (prefix === '' || prefix === '/' || !validRewritePath(prefix)) {
|
|
59
|
+
throw new TypeError(
|
|
60
|
+
'Invalid config: pages.rewrite destination needs a static internal prefix',
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (rule.host != null) {
|
|
64
|
+
if (!rule.host || typeof rule.host !== 'object' || Array.isArray(rule.host)) {
|
|
65
|
+
throw new TypeError('Invalid config: pages.rewrite host must be an object');
|
|
66
|
+
}
|
|
67
|
+
for (const key of Object.keys(rule.host)) {
|
|
68
|
+
if (!['include', 'exclude'].includes(key)) {
|
|
69
|
+
throw new TypeError(`Invalid config: pages.rewrite host.${key} is not supported`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const key of ['include', 'exclude']) {
|
|
73
|
+
if (
|
|
74
|
+
rule.host[key] != null &&
|
|
75
|
+
(!Array.isArray(rule.host[key]) ||
|
|
76
|
+
rule.host[key].some((host) => typeof host !== 'string' || host.length === 0))
|
|
77
|
+
) {
|
|
78
|
+
throw new TypeError(
|
|
79
|
+
`Invalid config: pages.rewrite host.${key} must be an array of hosts`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
15
86
|
|
|
16
87
|
function resolveHmr(rawHmr) {
|
|
17
88
|
if (rawHmr == null) return undefined;
|
|
@@ -80,9 +151,10 @@ function resolve(config, { rootDir } = {}) {
|
|
|
80
151
|
throw new TypeError('Invalid config: pages.language must be a valid language tag');
|
|
81
152
|
}
|
|
82
153
|
if (pages.rewrite != null) {
|
|
83
|
-
if (
|
|
154
|
+
if (Array.isArray(pages.rewrite)) {
|
|
155
|
+
validateRewriteRules(pages.rewrite);
|
|
156
|
+
} else if (
|
|
84
157
|
typeof pages.rewrite !== 'object' ||
|
|
85
|
-
Array.isArray(pages.rewrite) ||
|
|
86
158
|
typeof pages.rewrite.handler !== 'function' ||
|
|
87
159
|
typeof pages.rewrite.reservedPrefix !== 'string' ||
|
|
88
160
|
pages.rewrite.reservedPrefix === '/' ||
|
|
@@ -91,7 +163,7 @@ function resolve(config, { rootDir } = {}) {
|
|
|
91
163
|
/[?#\s]/.test(pages.rewrite.reservedPrefix)
|
|
92
164
|
) {
|
|
93
165
|
throw new TypeError(
|
|
94
|
-
'Invalid config: pages.rewrite requires a handler function
|
|
166
|
+
'Invalid config: pages.rewrite requires an array of static rules or a handler function with a reservedPrefix',
|
|
95
167
|
);
|
|
96
168
|
}
|
|
97
169
|
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { resolveSessionConfig } from '../../session/index.js';
|
|
2
2
|
|
|
3
3
|
function resolve(config, { mode } = {}) {
|
|
4
|
-
if (
|
|
4
|
+
if (config.session === false) return { ...config, session: undefined };
|
|
5
5
|
const vaultSession = config.vault?.values?.session;
|
|
6
|
+
if (config.session == null && !vaultSession) return config;
|
|
6
7
|
const sessionKeyring = config.vault?.keyring?.map((entry) => ({
|
|
7
8
|
id: entry.id,
|
|
8
9
|
password: entry.secrets.session,
|
|
9
10
|
}));
|
|
10
11
|
const sessionInput = {
|
|
11
|
-
...config.session,
|
|
12
|
+
...(config.session ?? {}),
|
|
12
13
|
password: vaultSession,
|
|
13
14
|
keyring: sessionKeyring,
|
|
14
15
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
function normalizePort(value, name) {
|
|
2
|
+
if (value === undefined || value === null || value === '') return value;
|
|
3
|
+
const port = typeof value === 'string' && /^\d+$/.test(value) ? Number(value) : value;
|
|
4
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
5
|
+
throw new TypeError(`Invalid config: ${name} must be an integer from 1 to 65535`);
|
|
6
|
+
}
|
|
7
|
+
return port;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export { normalizePort };
|
package/server/pages/handler.js
CHANGED
|
@@ -37,6 +37,7 @@ function createPagesHandler(options) {
|
|
|
37
37
|
const appContext = options.appContext ?? null;
|
|
38
38
|
const documentLanguage = options.documentLanguage;
|
|
39
39
|
const rewrite = options.rewrite ?? null;
|
|
40
|
+
const reservedPrefixes = getRewriteReservedPrefixes(rewrite);
|
|
40
41
|
const mode = options.mode ?? 'production';
|
|
41
42
|
const devMode = mode === 'development';
|
|
42
43
|
const viteDev = options.viteDev === true;
|
|
@@ -61,7 +62,7 @@ function createPagesHandler(options) {
|
|
|
61
62
|
rootDir,
|
|
62
63
|
outDir,
|
|
63
64
|
viteDev,
|
|
64
|
-
|
|
65
|
+
reservedPrefixes,
|
|
65
66
|
});
|
|
66
67
|
let lastSeenVersion = lazyContext ? manifest.version : 0;
|
|
67
68
|
const componentCache = new Map();
|
|
@@ -87,7 +88,7 @@ function createPagesHandler(options) {
|
|
|
87
88
|
rootDir,
|
|
88
89
|
outDir,
|
|
89
90
|
viteDev,
|
|
90
|
-
|
|
91
|
+
reservedPrefixes,
|
|
91
92
|
});
|
|
92
93
|
componentCache.clear();
|
|
93
94
|
cacheVersion++;
|
|
@@ -116,7 +117,7 @@ function createPagesHandler(options) {
|
|
|
116
117
|
rootDir,
|
|
117
118
|
outDir,
|
|
118
119
|
viteDev,
|
|
119
|
-
|
|
120
|
+
reservedPrefixes,
|
|
120
121
|
});
|
|
121
122
|
componentCache.clear();
|
|
122
123
|
cacheVersion++;
|
|
@@ -508,6 +509,7 @@ function normalizeHost(value) {
|
|
|
508
509
|
|
|
509
510
|
async function resolvePageRewrite(rewrite, ctx) {
|
|
510
511
|
if (!rewrite) return { pathname: ctx.page.pathname };
|
|
512
|
+
if (Array.isArray(rewrite)) return resolveStaticPageRewrite(rewrite, ctx);
|
|
511
513
|
const { reservedPrefix, handler } = rewrite;
|
|
512
514
|
if (
|
|
513
515
|
!ctx.page.host ||
|
|
@@ -529,6 +531,88 @@ async function resolvePageRewrite(rewrite, ctx) {
|
|
|
529
531
|
return { pathname: result.pathname };
|
|
530
532
|
}
|
|
531
533
|
|
|
534
|
+
function getRewriteReservedPrefixes(rewrite) {
|
|
535
|
+
if (!rewrite) return [];
|
|
536
|
+
if (!Array.isArray(rewrite)) return [rewrite.reservedPrefix];
|
|
537
|
+
return [
|
|
538
|
+
...new Set(
|
|
539
|
+
rewrite.map(({ destination }) => {
|
|
540
|
+
const tokenIndex = destination.indexOf(':');
|
|
541
|
+
return destination
|
|
542
|
+
.slice(0, tokenIndex < 0 ? destination.length : tokenIndex)
|
|
543
|
+
.replace(/\/$/, '');
|
|
544
|
+
}),
|
|
545
|
+
),
|
|
546
|
+
];
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function isReservedPath(pathname, reservedPrefixes) {
|
|
550
|
+
return reservedPrefixes.some(
|
|
551
|
+
(prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`),
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function normalizeConfiguredHost(host) {
|
|
556
|
+
return normalizeHost(host);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function matchesRewriteHost(host, condition) {
|
|
560
|
+
if (!condition) return true;
|
|
561
|
+
const includes = condition.include?.map(normalizeConfiguredHost);
|
|
562
|
+
const excludes = condition.exclude?.map(normalizeConfiguredHost);
|
|
563
|
+
if (includes && !includes.includes(host)) return false;
|
|
564
|
+
return !excludes?.includes(host);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function escapeRegex(value) {
|
|
568
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function matchRewriteSource(source, pathname) {
|
|
572
|
+
const names = [];
|
|
573
|
+
const segments = source.split('/').slice(1);
|
|
574
|
+
let pattern = '^';
|
|
575
|
+
for (const [index, segment] of segments.entries()) {
|
|
576
|
+
const token = segment.match(/^:([A-Za-z][A-Za-z0-9]*)(\*)?$/);
|
|
577
|
+
if (!token) {
|
|
578
|
+
pattern += `/${escapeRegex(segment)}`;
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
names.push(token[1]);
|
|
582
|
+
if (token[2]) {
|
|
583
|
+
pattern += index === segments.length - 1 ? '(?:/(.*))?' : '/(.*)';
|
|
584
|
+
} else {
|
|
585
|
+
pattern += '/([^/]+)';
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
const match = pathname.match(new RegExp(`${pattern}/?$`));
|
|
589
|
+
if (!match) return null;
|
|
590
|
+
return Object.fromEntries(names.map((name, index) => [name, match[index + 1] ?? '']));
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function interpolateRewriteDestination(destination, host, params) {
|
|
594
|
+
return destination.replace(/:([A-Za-z][A-Za-z0-9]*)(\*)?/g, (_match, name) => {
|
|
595
|
+
if (name === 'host') return encodeURIComponent(host);
|
|
596
|
+
return params[name] ?? '';
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function resolveStaticPageRewrite(rewrite, ctx) {
|
|
601
|
+
const reservedPrefixes = getRewriteReservedPrefixes(rewrite);
|
|
602
|
+
if (!ctx.page.host || isReservedPath(ctx.page.pathname, reservedPrefixes)) {
|
|
603
|
+
return { notFound: true };
|
|
604
|
+
}
|
|
605
|
+
for (const rule of rewrite) {
|
|
606
|
+
if (!matchesRewriteHost(ctx.page.host, rule.host)) continue;
|
|
607
|
+
const params = matchRewriteSource(rule.source, ctx.page.pathname);
|
|
608
|
+
if (!params) continue;
|
|
609
|
+
const pathname = interpolateRewriteDestination(rule.destination, ctx.page.host, params);
|
|
610
|
+
if (isReservedPath(pathname, reservedPrefixes)) return { pathname };
|
|
611
|
+
throw new TypeError('Page rewrite must target its configured internal prefix');
|
|
612
|
+
}
|
|
613
|
+
return { pathname: ctx.page.pathname };
|
|
614
|
+
}
|
|
615
|
+
|
|
532
616
|
async function runPageLoader(route, params, outDir, componentCache, cacheVersion, ctx) {
|
|
533
617
|
const bundlePath = path.join(outDir, route.serverBundle);
|
|
534
618
|
const pageModule = await loadPageModule(
|
|
@@ -619,16 +703,24 @@ function assertJsonSerializable(value, label, seen = new Set()) {
|
|
|
619
703
|
}
|
|
620
704
|
seen.delete(value);
|
|
621
705
|
}
|
|
622
|
-
function buildClientManifestJson(
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
706
|
+
function buildClientManifestJson(
|
|
707
|
+
manifest,
|
|
708
|
+
{
|
|
709
|
+
rootDir,
|
|
710
|
+
viteDev,
|
|
711
|
+
reservedPrefix,
|
|
712
|
+
reservedPrefixes = reservedPrefix ? [reservedPrefix] : [],
|
|
713
|
+
} = {},
|
|
714
|
+
) {
|
|
715
|
+
const clientVisibleManifest =
|
|
716
|
+
reservedPrefixes.length > 0
|
|
717
|
+
? {
|
|
718
|
+
...manifest,
|
|
719
|
+
entries: manifest.entries.filter(
|
|
720
|
+
(entry) => !isReservedPath(entry.pattern, reservedPrefixes),
|
|
721
|
+
),
|
|
722
|
+
}
|
|
723
|
+
: manifest;
|
|
632
724
|
if (viteDev) {
|
|
633
725
|
return buildViteClientManifestJson(clientVisibleManifest, rootDir);
|
|
634
726
|
}
|
package/server/pages/vite-dev.js
CHANGED
|
@@ -224,7 +224,7 @@ function getViteServerOptions(config, hmrServer) {
|
|
|
224
224
|
const hmr = config?.pages?.vite?.hmr;
|
|
225
225
|
return {
|
|
226
226
|
middlewareMode: true,
|
|
227
|
-
|
|
227
|
+
hmr: { ...hmr, server: hmrServer },
|
|
228
228
|
};
|
|
229
229
|
}
|
|
230
230
|
|
|
@@ -239,7 +239,7 @@ async function createViteHmrServer(log, { host = '0.0.0.0', port = 24678 } = {})
|
|
|
239
239
|
|
|
240
240
|
async function createViteDevRouter({ rootDir, log, config }) {
|
|
241
241
|
const appAliases = resolveAppAliases(rootDir);
|
|
242
|
-
const hmrServer =
|
|
242
|
+
const hmrServer = await createViteHmrServer(log);
|
|
243
243
|
let vite;
|
|
244
244
|
try {
|
|
245
245
|
vite = await createViteServer({
|