arcway 0.4.18 → 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/pages.js +75 -3
- package/server/pages/handler.js +105 -13
package/package.json
CHANGED
|
@@ -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
|
}
|
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
|
}
|