@ultimat3/cli 29.0.0 → 29.1.0

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": "29.0.0",
3
+ "version": "29.1.0",
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.7",
42
- "@ultimat3/action": "29.0.0",
43
- "@ultimat3/admin": "29.0.0",
44
- "@ultimat3/ai": "29.0.0",
45
- "@ultimat3/auth": "29.0.0",
46
- "@ultimat3/cache": "29.0.0",
47
- "@ultimat3/core": "29.0.0",
48
- "@ultimat3/db": "29.0.0",
49
- "@ultimat3/entity": "29.0.0",
50
- "@ultimat3/flags": "29.0.0",
51
- "@ultimat3/http": "29.0.0",
52
- "@ultimat3/i18n": "29.0.0",
53
- "@ultimat3/jobs": "29.0.0",
54
- "@ultimat3/mail": "29.0.0",
55
- "@ultimat3/manifest": "29.0.0",
56
- "@ultimat3/mcp": "29.0.0",
57
- "@ultimat3/money": "29.0.0",
58
- "@ultimat3/notify": "29.0.0",
59
- "@ultimat3/policy": "29.0.0",
60
- "@ultimat3/pwa": "29.0.0",
61
- "@ultimat3/query": "29.0.0",
62
- "@ultimat3/realtime": "29.0.0",
63
- "@ultimat3/render": "29.0.0",
64
- "@ultimat3/schema": "29.0.0",
65
- "@ultimat3/seo": "29.0.0",
66
- "@ultimat3/storage": "29.0.0",
67
- "@ultimat3/testing": "29.0.0",
68
- "@ultimat3/time": "29.0.0",
42
+ "@ultimat3/action": "29.1.0",
43
+ "@ultimat3/admin": "29.1.0",
44
+ "@ultimat3/ai": "29.1.0",
45
+ "@ultimat3/auth": "29.1.0",
46
+ "@ultimat3/cache": "29.1.0",
47
+ "@ultimat3/core": "29.1.0",
48
+ "@ultimat3/db": "29.1.0",
49
+ "@ultimat3/entity": "29.1.0",
50
+ "@ultimat3/flags": "29.1.0",
51
+ "@ultimat3/http": "29.1.0",
52
+ "@ultimat3/i18n": "29.1.0",
53
+ "@ultimat3/jobs": "29.1.0",
54
+ "@ultimat3/mail": "29.1.0",
55
+ "@ultimat3/manifest": "29.1.0",
56
+ "@ultimat3/mcp": "29.1.0",
57
+ "@ultimat3/money": "29.1.0",
58
+ "@ultimat3/notify": "29.1.0",
59
+ "@ultimat3/policy": "29.1.0",
60
+ "@ultimat3/pwa": "29.1.0",
61
+ "@ultimat3/query": "29.1.0",
62
+ "@ultimat3/realtime": "29.1.0",
63
+ "@ultimat3/render": "29.1.0",
64
+ "@ultimat3/schema": "29.1.0",
65
+ "@ultimat3/seo": "29.1.0",
66
+ "@ultimat3/storage": "29.1.0",
67
+ "@ultimat3/testing": "29.1.0",
68
+ "@ultimat3/time": "29.1.0",
69
69
  "babel-preset-solid": "^1.9.16"
70
70
  }
71
71
  }
@@ -11,10 +11,14 @@ import type { Ctx, NavigationSurface } from '@ultimat3/core';
11
11
  import { CLIENT_NAVIGATION_SCOPE_HEADER } from '@ultimat3/core';
12
12
  import type { Route, RouteNavigation, UltimateRequest } from '@ultimat3/http';
13
13
  import { locationFor, navigationPurpose, relocate } from '@ultimat3/http';
14
+ import type { Translator } from '@ultimat3/i18n';
14
15
  import type { ClientNavigationHead, HeadTag, RouteEntry } from '@ultimat3/render';
15
16
  import {
16
17
  clientNavigationTags,
17
18
  documentCarriesScope,
19
+ modalRouteList,
20
+ navigationModeOf,
21
+ navigationPrefetches,
18
22
  RouteNavigationInvalidError,
19
23
  routeEntries,
20
24
  } from '@ultimat3/render';
@@ -36,6 +40,11 @@ export interface NavigationDocumentHead {
36
40
  readonly buildId: string;
37
41
  /** `app.config.ts`'s `name`: two apps on one origin (web and admin) are two routers. */
38
42
  readonly app: string;
43
+ /**
44
+ * Per opted-in surface, its modal routes as `modalRouteList` orders them — what lets a click
45
+ * open the dialog before the answer. Absent or empty: the documents name none.
46
+ */
47
+ readonly modalRoutes?: ReadonlyMap<string, readonly string[]>;
39
48
  }
40
49
 
41
50
  export interface PageNavigation {
@@ -71,7 +80,7 @@ export function assertRouteNavigation(
71
80
  entries: readonly RouteEntry[] = routeEntries(),
72
81
  ): void {
73
82
  for (const entry of entries) {
74
- const mode = entry.config.navigation;
83
+ const mode = navigationModeOf(entry.config.navigation);
75
84
  if (mode === undefined || surfaces.some((surface) => surface === entry.surface)) continue;
76
85
  throw new RouteNavigationInvalidError(
77
86
  `${entry.file} declares navigation: '${mode}', and its surface (${entry.surface}) is not in app.config.ts navigation.client`,
@@ -79,6 +88,28 @@ export function assertRouteNavigation(
79
88
  }
80
89
  }
81
90
 
91
+ /**
92
+ * Each opted-in surface's modal routes, from the whole table: a page of ANY surface may outrank a
93
+ * modal pattern on the shared URL space, and the router must not open a frame the server will
94
+ * answer with that page.
95
+ */
96
+ export function modalRoutesOf(
97
+ surfaces: readonly NavigationSurface[],
98
+ entries: readonly RouteEntry[] = routeEntries(),
99
+ ): ReadonlyMap<string, readonly string[]> {
100
+ return new Map(
101
+ surfaces.map((surface) => [
102
+ surface,
103
+ modalRouteList(
104
+ entries.map((entry) => ({
105
+ path: entry.path,
106
+ modal: entry.surface === surface && navigationModeOf(entry.config.navigation) === 'modal',
107
+ })),
108
+ ),
109
+ ]),
110
+ );
111
+ }
112
+
82
113
  /**
83
114
  * Built at boot, never on the watcher tick — the router is framework code, and a tab that loaded
84
115
  * it keeps it across every soft navigation, so a URL that changed per save would be one no tab
@@ -109,6 +140,7 @@ export async function pageNavigation(
109
140
  scriptUrl: script.url,
110
141
  buildId,
111
142
  app: declared.app,
143
+ modalRoutes: modalRoutesOf(declared.surfaces),
112
144
  },
113
145
  };
114
146
  }
@@ -117,13 +149,25 @@ export async function pageNavigation(
117
149
  export const surfaceKey = (head: NavigationDocumentHead, surface: string): string =>
118
150
  `${head.app}:${surface}`;
119
151
 
120
- /** The head for one document, or `undefined` when its surface did not opt in. */
152
+ /**
153
+ * The head for one document, or `undefined` when its surface did not opt in. With `t`, and modal
154
+ * routes on the surface, it names them and the frame's two words in the document's own locale
155
+ * (the framework catalog's `ui.close` and `ui.loading`, which an app may override).
156
+ */
121
157
  export function navigationHeadFor(
122
158
  head: NavigationDocumentHead | undefined,
123
159
  surface: string,
160
+ t?: Translator,
124
161
  ): ClientNavigationHead | undefined {
125
162
  if (head === undefined || !head.surfaces.has(surface)) return undefined;
126
- return { surface: surfaceKey(head, surface), buildId: head.buildId, scriptUrl: head.scriptUrl };
163
+ const base = {
164
+ surface: surfaceKey(head, surface),
165
+ buildId: head.buildId,
166
+ scriptUrl: head.scriptUrl,
167
+ };
168
+ const routes = head.modalRoutes?.get(surface) ?? [];
169
+ if (t === undefined || routes.length === 0) return base;
170
+ return { ...base, modals: { routes, close: t('ui.close'), loading: t('ui.loading') } };
127
171
  }
128
172
 
129
173
  /**
@@ -137,15 +181,15 @@ export function navigationHeadFor(
137
181
  export function navigationTagsOf(
138
182
  head: NavigationDocumentHead | undefined,
139
183
  entry: Pick<RouteEntry, 'surface' | 'config'>,
184
+ t?: Translator,
140
185
  ): readonly HeadTag[] {
141
- if (entry.config.navigation === 'document') return [];
142
- const tags = navigationHeadFor(head, entry.surface);
186
+ const mode = navigationModeOf(entry.config.navigation);
187
+ if (mode === 'document') return [];
188
+ const tags = navigationHeadFor(head, entry.surface, t);
143
189
  if (tags === undefined) return [];
144
190
  // A modal says so in its own document: the router shows it over the page beneath, never decided
145
191
  // from a link — a redirect's target and a pasted `#/…` address are answered alike.
146
- return clientNavigationTags(
147
- entry.config.navigation === 'modal' ? { ...tags, modal: true } : tags,
148
- );
192
+ return clientNavigationTags(mode === 'modal' ? { ...tags, modal: true } : tags);
149
193
  }
150
194
 
151
195
  /** `{ navigation }` for `metaOf`'s spread, or nothing. */
@@ -167,10 +211,10 @@ export function routeNavigationOf(
167
211
  head: NavigationDocumentHead | undefined,
168
212
  ): RouteNavigation | undefined {
169
213
  if (head === undefined || !head.surfaces.has(entry.surface)) return undefined;
170
- if (entry.config.navigation === 'document') return undefined;
214
+ if (navigationModeOf(entry.config.navigation) === 'document') return undefined;
171
215
  return {
172
216
  surface: surfaceKey(head, entry.surface),
173
- prefetch: entry.config.navigation === 'prefetch',
217
+ prefetch: navigationPrefetches(entry.config.navigation),
174
218
  };
175
219
  }
176
220
 
@@ -12,6 +12,8 @@ import type { AppLocaleSet } from '@ultimat3/i18n/app-catalogs';
12
12
  import type { RouteDescriptor, RouteEntry } from '@ultimat3/render';
13
13
  import {
14
14
  describePages,
15
+ navigationModeOf,
16
+ navigationPrefetches,
15
17
  renderHead,
16
18
  routeEntries,
17
19
  speculationPattern,
@@ -37,8 +39,9 @@ export async function loadSpeculation(root: string): Promise<SpeculationConfig>
37
39
  * no policy, no `no-store`. Such a page ran its `load` at build time, for nobody. An `ssr` page
38
40
  * there has no way to declare that its GET records something (`navigation` is refused on a
39
41
  * surface that did not opt in), so it is never a candidate.
40
- * - On a client-routed surface: only a page that declared `navigation: 'prefetch'` — the app's own
41
- * statement that its GET is a pure read, the same one the router's prefetch obeys.
42
+ * - On a client-routed surface: only a page that declared `navigation: 'prefetch'` (or a modal
43
+ * that declared `prefetch: 'intent'`) — the app's own statement that its GET is a pure read,
44
+ * the same one the router's prefetch obeys.
42
45
  * - Never an `api/` route, a `navigation: 'document'` page, or an `offline: 'network-only'` one.
43
46
  */
44
47
  export function speculationCandidate(
@@ -47,8 +50,9 @@ export function speculationCandidate(
47
50
  client: ReadonlySet<string>,
48
51
  ): boolean {
49
52
  if (entry.surface !== 'site' && entry.surface !== 'app') return false;
50
- if (entry.config.navigation === 'document' || described.offline === 'network-only') return false;
51
- if (client.has(entry.surface)) return entry.config.navigation === 'prefetch';
53
+ const mode = navigationModeOf(entry.config.navigation);
54
+ if (mode === 'document' || described.offline === 'network-only') return false;
55
+ if (client.has(entry.surface)) return navigationPrefetches(entry.config.navigation);
52
56
  return (described.mode === 'static' || described.mode === 'isr') && !described.personal;
53
57
  }
54
58
 
@@ -120,7 +120,7 @@ const headFor = async (
120
120
  ): Promise<string> => {
121
121
  const meta = metaContextFor(ctx, data);
122
122
  const url = new URL(ctx.url);
123
- const router = navigationTagsOf(options.navigation, entry);
123
+ const router = navigationTagsOf(options.navigation, entry, meta.t);
124
124
  return (
125
125
  renderHead(
126
126
  headFromMeta(