@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 +28 -28
- package/src/page-navigation.ts +54 -10
- package/src/page-speculation.ts +8 -4
- package/src/runtime-render.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/cli",
|
|
3
|
-
"version": "29.
|
|
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.
|
|
43
|
-
"@ultimat3/admin": "29.
|
|
44
|
-
"@ultimat3/ai": "29.
|
|
45
|
-
"@ultimat3/auth": "29.
|
|
46
|
-
"@ultimat3/cache": "29.
|
|
47
|
-
"@ultimat3/core": "29.
|
|
48
|
-
"@ultimat3/db": "29.
|
|
49
|
-
"@ultimat3/entity": "29.
|
|
50
|
-
"@ultimat3/flags": "29.
|
|
51
|
-
"@ultimat3/http": "29.
|
|
52
|
-
"@ultimat3/i18n": "29.
|
|
53
|
-
"@ultimat3/jobs": "29.
|
|
54
|
-
"@ultimat3/mail": "29.
|
|
55
|
-
"@ultimat3/manifest": "29.
|
|
56
|
-
"@ultimat3/mcp": "29.
|
|
57
|
-
"@ultimat3/money": "29.
|
|
58
|
-
"@ultimat3/notify": "29.
|
|
59
|
-
"@ultimat3/policy": "29.
|
|
60
|
-
"@ultimat3/pwa": "29.
|
|
61
|
-
"@ultimat3/query": "29.
|
|
62
|
-
"@ultimat3/realtime": "29.
|
|
63
|
-
"@ultimat3/render": "29.
|
|
64
|
-
"@ultimat3/schema": "29.
|
|
65
|
-
"@ultimat3/seo": "29.
|
|
66
|
-
"@ultimat3/storage": "29.
|
|
67
|
-
"@ultimat3/testing": "29.
|
|
68
|
-
"@ultimat3/time": "29.
|
|
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
|
}
|
package/src/page-navigation.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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
|
|
217
|
+
prefetch: navigationPrefetches(entry.config.navigation),
|
|
174
218
|
};
|
|
175
219
|
}
|
|
176
220
|
|
package/src/page-speculation.ts
CHANGED
|
@@ -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'`
|
|
41
|
-
* statement that its GET is a pure read,
|
|
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
|
-
|
|
51
|
-
if (
|
|
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
|
|
package/src/runtime-render.ts
CHANGED
|
@@ -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(
|