@escape-game-over/atlas 0.1.70 → 0.1.71

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/README.md CHANGED
@@ -119,6 +119,7 @@ it returns and nothing else, resolves it, and writes `_redirects`:
119
119
  // projects/rome/redirects.ts
120
120
  export default redirects([
121
121
  { from: "/jobs", to: { route: "careers" }, kind: "permanent" }, // ✗ if Rome has no careers page
122
+ { from: "/en/*", to: { route: "home", locale: "en-US", splat: true }, kind: "permanent" }, // /en/faq → /en-US/faq
122
123
  ])
123
124
  ```
124
125
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escape-game-over/atlas",
3
- "version": "0.1.70",
3
+ "version": "0.1.71",
4
4
  "type": "module",
5
5
  "description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
6
6
  "private": false,
package/src/redirects.ts CHANGED
@@ -71,13 +71,19 @@ export type SitePath = UrlPath;
71
71
  */
72
72
  export type ExternalUrl = HttpsUrl;
73
73
 
74
+ /** A `splat` target whose source has no `*` to carry: there is no rest to keep. */
75
+ export interface SplatNeedsWildcard<From extends string> {
76
+ readonly __SPLAT_WITHOUT_WILDCARD__: `"${From}" has no "/*" to carry over: a rule with \`splat: true\` needs a source ending in "/*"`;
77
+ }
78
+
74
79
  /**
75
- * Rejects an external target that is not `https://`, naming it.
80
+ * Rejects an external target that is not `https://`, and a `splat` target
81
+ * whose source has no wildcard, naming each.
76
82
  *
77
- * Applied to the rules array in parameter position, so a bad URL reports on its
78
- * own line rather than as the whole array failing to match. The check itself is
79
- * `ValidHttpsUrl`, shared with the site origin — one idea of an acceptable URL,
80
- * stated once.
83
+ * Applied to the rules array in parameter position, so a bad rule reports on
84
+ * its own line rather than as the whole array failing to match. The URL check
85
+ * itself is `ValidHttpsUrl`, shared with the site origin — one idea of an
86
+ * acceptable URL, stated once.
81
87
  */
82
88
  export type ValidateRedirectTargets<Rules> = {
83
89
  readonly [K in keyof Rules]: Rules[K] extends {
@@ -90,7 +96,18 @@ export type ValidateRedirectTargets<Rules> = {
90
96
  ? ValidHttpsUrl<To>
91
97
  : Rules[K][P];
92
98
  }
93
- : Rules[K];
99
+ : Rules[K] extends {
100
+ readonly from: infer From extends string;
101
+ readonly to: { readonly splat: true };
102
+ }
103
+ ? From extends `${string}/*`
104
+ ? Rules[K]
105
+ : {
106
+ readonly [P in keyof Rules[K]]: P extends "from"
107
+ ? SplatNeedsWildcard<From>
108
+ : Rules[K][P];
109
+ }
110
+ : Rules[K];
94
111
  };
95
112
 
96
113
  /**
@@ -105,7 +122,28 @@ export type ValidateRedirectTargets<Rules> = {
105
122
  */
106
123
  export type RedirectTarget<Id extends string, L extends string> =
107
124
  | ExternalUrl
108
- | { readonly route: Id; readonly locale?: L; readonly page?: number }
125
+ | {
126
+ readonly route: Id;
127
+ readonly locale?: L;
128
+ readonly page?: number;
129
+ readonly splat?: never;
130
+ }
131
+ /**
132
+ * A page, with whatever the source's `*` matched kept after its path:
133
+ * `/en/*` to `{ route: "home", locale: "en-US", splat: true }` sends
134
+ * `/en/faq` to `/en-US/faq`.
135
+ *
136
+ * For an old prefix whose pages kept their slugs, so one rule answers every
137
+ * URL under it. The target stays a route, so the prefix it lands on is
138
+ * derived like every other link rather than spelled out by hand. Not with
139
+ * `page`: the rest of a path cannot follow a page number.
140
+ */
141
+ | {
142
+ readonly route: Id;
143
+ readonly locale?: L;
144
+ readonly splat: true;
145
+ readonly page?: never;
146
+ }
109
147
  /**
110
148
  * A file served verbatim from `public/` — a PDF, a spreadsheet.
111
149
  *
@@ -610,6 +610,20 @@ export function createSite<
610
610
  to: ((): string => {
611
611
  if (typeof rule.to === "string") return rule.to;
612
612
  if ("file" in rule.to) return rule.to.file;
613
+ if (rule.to.splat === true) {
614
+ // Checked here too, for a rule the types could not see:
615
+ // without a `*`, the host has nothing to put in `:splat`.
616
+ if (!rule.from.endsWith("/*")) {
617
+ throw new Error(
618
+ `Redirect "${rule.from}" keeps the rest of the path (\`splat: true\`) but has no "/*" to take it from.`
619
+ );
620
+ }
621
+ const path = pathFor(
622
+ rule.to.route,
623
+ rule.to.locale ?? defaultLocale
624
+ );
625
+ return path === "/" ? "/:splat" : `${path}/:splat`;
626
+ }
613
627
  return pathFor(rule.to.route, rule.to.locale ?? defaultLocale, {
614
628
  page: rule.to.page,
615
629
  });