@ultimat3/cli 27.8.0 → 27.8.1

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": "@ultimat3/cli",
3
- "version": "27.8.0",
3
+ "version": "27.8.1",
4
4
  "description": "The `x` binary: new, dev, build, verify, generate, db, mcp, doctor, deploy",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -39,33 +39,33 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@babel/core": "^8.0.1",
42
- "@ultimat3/action": "27.8.0",
43
- "@ultimat3/admin": "27.8.0",
44
- "@ultimat3/ai": "27.8.0",
45
- "@ultimat3/auth": "27.8.0",
46
- "@ultimat3/cache": "27.8.0",
47
- "@ultimat3/core": "27.8.0",
48
- "@ultimat3/db": "27.8.0",
49
- "@ultimat3/entity": "27.8.0",
50
- "@ultimat3/flags": "27.8.0",
51
- "@ultimat3/http": "27.8.0",
52
- "@ultimat3/i18n": "27.8.0",
53
- "@ultimat3/jobs": "27.8.0",
54
- "@ultimat3/mail": "27.8.0",
55
- "@ultimat3/manifest": "27.8.0",
56
- "@ultimat3/mcp": "27.8.0",
57
- "@ultimat3/money": "27.8.0",
58
- "@ultimat3/notify": "27.8.0",
59
- "@ultimat3/policy": "27.8.0",
60
- "@ultimat3/pwa": "27.8.0",
61
- "@ultimat3/query": "27.8.0",
62
- "@ultimat3/realtime": "27.8.0",
63
- "@ultimat3/render": "27.8.0",
64
- "@ultimat3/schema": "27.8.0",
65
- "@ultimat3/seo": "27.8.0",
66
- "@ultimat3/storage": "27.8.0",
67
- "@ultimat3/testing": "27.8.0",
68
- "@ultimat3/time": "27.8.0",
42
+ "@ultimat3/action": "27.8.1",
43
+ "@ultimat3/admin": "27.8.1",
44
+ "@ultimat3/ai": "27.8.1",
45
+ "@ultimat3/auth": "27.8.1",
46
+ "@ultimat3/cache": "27.8.1",
47
+ "@ultimat3/core": "27.8.1",
48
+ "@ultimat3/db": "27.8.1",
49
+ "@ultimat3/entity": "27.8.1",
50
+ "@ultimat3/flags": "27.8.1",
51
+ "@ultimat3/http": "27.8.1",
52
+ "@ultimat3/i18n": "27.8.1",
53
+ "@ultimat3/jobs": "27.8.1",
54
+ "@ultimat3/mail": "27.8.1",
55
+ "@ultimat3/manifest": "27.8.1",
56
+ "@ultimat3/mcp": "27.8.1",
57
+ "@ultimat3/money": "27.8.1",
58
+ "@ultimat3/notify": "27.8.1",
59
+ "@ultimat3/policy": "27.8.1",
60
+ "@ultimat3/pwa": "27.8.1",
61
+ "@ultimat3/query": "27.8.1",
62
+ "@ultimat3/realtime": "27.8.1",
63
+ "@ultimat3/render": "27.8.1",
64
+ "@ultimat3/schema": "27.8.1",
65
+ "@ultimat3/seo": "27.8.1",
66
+ "@ultimat3/storage": "27.8.1",
67
+ "@ultimat3/testing": "27.8.1",
68
+ "@ultimat3/time": "27.8.1",
69
69
  "babel-preset-solid": "^1.9.15"
70
70
  }
71
71
  }
@@ -0,0 +1,16 @@
1
+ // The URL a SHARED document (`static`, `isr`) is rendered at and kept under: the request's own, at
2
+ // the one spelling of the address the router matched. The router answers `/about/`, `//about` and
3
+ // `/%61bout` with one page; a document one visitor's spelling rendered and the next visitor is
4
+ // answered must not carry the first one's spelling in its `canonical`, nor be kept once per spelling.
5
+
6
+ import type { RequestContext } from '@ultimat3/http';
7
+ import { requestAddress } from '@ultimat3/http';
8
+
9
+ /** `url` with its pathname respelled (`requestAddress`); unchanged before a route matched. */
10
+ export function addressedUrl(url: URL, ctx: RequestContext): URL {
11
+ const address = requestAddress(ctx);
12
+ if (address === undefined || address.pathname === url.pathname) return url;
13
+ const at = new URL(url);
14
+ at.pathname = address.pathname;
15
+ return at;
16
+ }
@@ -4,11 +4,11 @@
4
4
 
5
5
  import { logger } from '@ultimat3/core';
6
6
  import type { RedirectIntent, RequestContext, RouteParams } from '@ultimat3/http';
7
- import { asCtx, takeRedirect } from '@ultimat3/http';
7
+ import { asCtx, requestAddress, takeRedirect } from '@ultimat3/http';
8
8
  import type { RenderResult, RouteData, RouteEntry } from '@ultimat3/render';
9
- import { routeDataFor, routeNoStoreOf, routeStatusOf } from '@ultimat3/render';
9
+ import { describePages, routeDataFor, routeNoStoreOf, routeStatusOf } from '@ultimat3/render';
10
10
  import type { IsrController, IsrEntry } from '@ultimat3/render/server';
11
- import { isrKey, isrRequestUrl, undeclaredQuery } from '@ultimat3/render/server';
11
+ import { isrKey, isrRequestUrl, isrRouteFor, undeclaredQuery } from '@ultimat3/render/server';
12
12
 
13
13
  export type IsrOutcome =
14
14
  | { readonly kind: 'page'; readonly result: RenderResult }
@@ -38,15 +38,17 @@ const NAMED_PARAMS = 5;
38
38
  const NAMED_PARAM_CHARS = 40;
39
39
 
40
40
  /**
41
- * The request narrowed to what the route declared it varies on (`revalidate.query`). ONE narrowing
42
- * feeds the store key, `load`, `meta` and the page, so a parameter outside the key cannot reach
43
- * the document stored under it.
41
+ * The request narrowed to what the route declared it varies on (`revalidate.query`), AT the one
42
+ * spelling of its address (`pathname`). ONE narrowing feeds the store key, `load`, `meta` and the
43
+ * page, so neither a parameter outside the key nor the way a visitor happened to spell the path
44
+ * (`/blog//a/`, `/blog/%61`) can reach the document stored under it.
44
45
  *
45
46
  * A route that declared NOTHING still keys on the whole query, and is told so once, the first time
46
47
  * a visitor uses that: every distinct query string is then a render and a stored page of its own.
47
48
  */
48
- function keyedData(entry: RouteEntry, data: IsrData): IsrData {
49
+ function keyedData(entry: RouteEntry, data: IsrData, pathname: string | undefined): IsrData {
49
50
  const url = new URL(data.url);
51
+ if (pathname !== undefined) url.pathname = pathname;
50
52
  const declared = entry.config.revalidate?.query ?? null;
51
53
  if (declared === null && !toldUndeclared.has(entry.file)) {
52
54
  const minted = undeclaredQuery(url, declared);
@@ -96,12 +98,27 @@ function dropPage(isr: IsrController, key: string): void {
96
98
  * redirected decides for itself, exactly as the static single-flight in `appRoutes` does.
97
99
  */
98
100
  export async function isrOutcome(arrived: IsrRequest): Promise<IsrOutcome> {
99
- const request: IsrRequest = { ...arrived, data: keyedData(arrived.entry, arrived.data) };
101
+ // Where the router MATCHED this request, never where the visitor wrote it: the router answers
102
+ // many spellings with one page, and a key built from the raw pathname stored each of them.
103
+ const address = requestAddress(arrived.ctx);
104
+ const request: IsrRequest = {
105
+ ...arrived,
106
+ data: keyedData(arrived.entry, arrived.data, address?.pathname),
107
+ };
100
108
  const { entry, data, ctx, isr } = request;
101
109
  // `isrKey(url, locale)`, never `url.pathname`: the DECLARED query is part of what was rendered —
102
110
  // `meta` reads `data.url` — so two URLs differing in it are two documents (#171). The locale is
103
- // `ctx.locale`, the one the `locale` stage negotiated for THIS request.
104
- const key = isrKey(new URL(data.url), asCtx(ctx).locale);
111
+ // `ctx.locale`, the one the `locale` stage negotiated for THIS request. The key's path carries
112
+ // NO base path: the route table has never heard of the mount, so a key that kept it matched no
113
+ // route — and every `isr` page of an app under a `basePath` was stored with no ttl and no tags.
114
+ const keyed = new URL(data.url);
115
+ if (address !== undefined) keyed.pathname = address.path;
116
+ const key = isrKey(keyed, asCtx(ctx).locale);
117
+ // The key has to read back as THIS route, or its ttl, its tags and what a bust does to it are
118
+ // another route's or nobody's. It always does for an address the framework's URL scheme can
119
+ // spell; what is left is a path the router matched that another reading claims (`//en/x` is the
120
+ // `/:lang/x` route with no locale, and `/en/x` is the `/x` route in English). Answered, uncached.
121
+ if (isrRouteFor(describePages(), key)?.file !== entry.file) return ownOutcome(request);
105
122
  let decided: RedirectIntent | undefined;
106
123
  // `{ html, status }`, never the bare string: the entry stores the status beside the HTML and
107
124
  // serves it on every hit, so a 404 under `isr` is a 404 for its whole TTL.
@@ -127,9 +144,9 @@ export async function isrOutcome(arrived: IsrRequest): Promise<IsrOutcome> {
127
144
  }
128
145
 
129
146
  /**
130
- * A joiner whose leader's load redirected: its own `load`, answered for this request alone. A page
131
- * here is NOT stored — the store was just told the key has none — so it is `private, no-store`;
132
- * the next request to miss stores one.
147
+ * A request answered for itself alone: a joiner whose leader's load redirected, or one whose key
148
+ * is not its own route's. Its own `load`; a page here is NOT stored — the store was just told the
149
+ * key has none, or the key is another page's — so it is `private, no-store`.
133
150
  */
134
151
  async function ownOutcome(request: IsrRequest): Promise<IsrOutcome> {
135
152
  const loaded = await routeDataFor(request.entry.config, request.data);
@@ -53,6 +53,7 @@ import {
53
53
  principalRelocation,
54
54
  } from './page-navigation';
55
55
  import { bootScript, collectorFor } from './route-islands';
56
+ import { addressedUrl } from './runtime-address';
56
57
  import { attachedIsr } from './runtime-isr';
57
58
  import { isrOutcome } from './runtime-isr-outcome';
58
59
  import type { StaticResult } from './static-document';
@@ -425,7 +426,7 @@ export function appRoutes(options: DevRenderOptions): readonly Route[] {
425
426
  // A static page already rendered by this process is answered without `load` or a render.
426
427
  const key =
427
428
  options.memoStatic === true
428
- ? staticMemoKey(entry, request.url, asCtx(ctx).locale)
429
+ ? staticMemoKey(entry, addressedUrl(request.url, ctx), asCtx(ctx).locale)
429
430
  : undefined;
430
431
  const kept = key === undefined ? undefined : memo.get(key);
431
432
  if (kept !== undefined) return staticResponse(request, kept);
@@ -461,7 +462,9 @@ async function renderEntry(
461
462
  options: DevRenderOptions,
462
463
  isr: IsrController,
463
464
  ): Promise<Rendered> {
464
- const data: DevRouteData = { url: request.url.href, params: ctx.params };
465
+ // A `static` document is one file in the export, so it is rendered AT its one address here too.
466
+ const at = entry.config.render === 'static' ? addressedUrl(request.url, ctx) : request.url;
467
+ const data: DevRouteData = { url: at.href, params: ctx.params };
465
468
  if (entry.config.render === 'isr') return renderIsr(entry, data, ctx, options, isr);
466
469
  // ONCE per request, before the mode is chosen: every branch of `resultFor` reads this same
467
470
  // object, so a route's `load` runs exactly once however its mode splits head from body.
@@ -12,9 +12,11 @@ import { etagMatches } from './runtime-storage';
12
12
  export type StaticResult = RenderResult & { readonly body: string };
13
13
 
14
14
  /**
15
- * The entries one process keeps. A static route has no params here and no query, so the real count
16
- * is routes × locales; the cap is what a forged `Host` (part of the key when the app declares no
17
- * origin) can cost — past it a page is rendered per request, as before, never refused.
15
+ * The entries one process keeps. A static route has no params here and no query, and the key's
16
+ * path is the request's ADDRESS (`runtime-address.ts`), never its raw pathname — `/about/`,
17
+ * `//about` and `/%61bout` are one entry — so the real count is routes × locales; the cap is what a
18
+ * forged `Host` (part of the key when the app declares no origin) can cost — past it a page is
19
+ * rendered per request, as before, never refused.
18
20
  */
19
21
  export const STATIC_MEMO_LIMIT = 1024;
20
22
 
@@ -77,7 +79,9 @@ export function createStaticMemo(
77
79
  * The key a request's document is kept under, or `undefined` when it must be rendered: a query
78
80
  * string is part of what `meta` reads (`data.url`), and a dynamic segment is an unbounded set an
79
81
  * anonymous visitor chooses. The origin is in the key because canonical and `og:url` are absolute
80
- * against the request's own when the app declares none.
82
+ * against the request's own when the app declares none. `url` is `addressedUrl`'s: keyed on the
83
+ * pathname as written, any visitor filled the memo with spellings of one page (`/about//`, …) and
84
+ * every page that arrived after the cap was rendered per request for the life of the process.
81
85
  */
82
86
  export function staticMemoKey(
83
87
  entry: Pick<RouteEntry, 'config' | 'pattern'>,