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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcway",
3
- "version": "0.4.18",
3
+ "version": "0.4.19",
4
4
  "description": "A convention-based framework for building modular monoliths with strict domain boundaries.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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 and a reservedPrefix such as "/_sites"',
166
+ 'Invalid config: pages.rewrite requires an array of static rules or a handler function with a reservedPrefix',
95
167
  );
96
168
  }
97
169
  }
@@ -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
- reservedPrefix: rewrite?.reservedPrefix,
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
- reservedPrefix: rewrite?.reservedPrefix,
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
- reservedPrefix: rewrite?.reservedPrefix,
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(manifest, { rootDir, viteDev, reservedPrefix } = {}) {
623
- const clientVisibleManifest = reservedPrefix
624
- ? {
625
- ...manifest,
626
- entries: manifest.entries.filter(
627
- (entry) =>
628
- entry.pattern !== reservedPrefix && !entry.pattern.startsWith(`${reservedPrefix}/`),
629
- ),
630
- }
631
- : manifest;
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
  }