@antelopejs/dms-frontend 0.3.11 → 0.4.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/README.md +85 -5
- package/dist/commands/verify-source-action.js +2 -10
- package/dist/commands/verify-source.js +11 -7
- package/dist/config.d.ts +1 -0
- package/dist/config.js +5 -1
- package/dist/derived-outputs.d.ts +2 -0
- package/dist/derived-outputs.js +22 -3
- package/dist/help.js +4 -0
- package/dist/layers.js +1 -0
- package/dist/verify-source-runner.js +6 -42
- package/dist/verify-source-sources.d.ts +13 -0
- package/dist/verify-source-sources.js +189 -0
- package/dist/workspace.d.ts +1 -0
- package/package.json +1 -1
- package/templates/vue/DmsDynamicPage.vue +3 -1
- package/templates/vue/app-runtime.ts +42 -26
- package/templates/vue/frontend-build-loader.ts +78 -0
- package/templates/vue/frontend-build.ts +41 -0
- package/templates/vue/frontend-module.ts +166 -22
- package/templates/vue/icon-hydration.ts +78 -0
- package/templates/vue/server/auth/backend.mjs +20 -6
- package/templates/vue/server/auth/client-ip.mjs +28 -0
- package/templates/vue/server/auth/routes.mjs +1 -0
- package/templates/vue/server/auth/session.mjs +6 -0
- package/templates/vue/server/icons.mjs +43 -1
- package/templates/vue/server.mjs +14 -13
- package/templates/vue/ssr-renderer.ts +18 -1
- package/templates/vue/vite.config.ts +25 -23
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
type PropType,
|
|
32
32
|
type Ref,
|
|
33
33
|
reactive,
|
|
34
|
+
readonly,
|
|
34
35
|
ref,
|
|
35
36
|
watch,
|
|
36
37
|
} from "vue";
|
|
@@ -50,6 +51,12 @@ export interface DmsPagePayload {
|
|
|
50
51
|
layoutUrl?: string;
|
|
51
52
|
layout?: DmsPageLayout;
|
|
52
53
|
shared?: DmsSharedPagePayload;
|
|
54
|
+
/**
|
|
55
|
+
* The frontend module that owns the page's component tree, by the name the
|
|
56
|
+
* backend added it under (its manifest name). Components that module
|
|
57
|
+
* registered with `{ private: true }` resolve in this page's tree only.
|
|
58
|
+
*/
|
|
59
|
+
module?: string;
|
|
53
60
|
[key: string]: unknown;
|
|
54
61
|
}
|
|
55
62
|
export interface DmsSharedPagePayload {
|
|
@@ -82,6 +89,13 @@ export interface DmsUser extends Record<string, unknown> {}
|
|
|
82
89
|
export interface DmsSession {
|
|
83
90
|
accountId: string;
|
|
84
91
|
activeTenantId?: string;
|
|
92
|
+
/**
|
|
93
|
+
* When the frontend server last wrote this session, in milliseconds since
|
|
94
|
+
* the epoch. A page restored from history or from the prefetch cache
|
|
95
|
+
* carries an older session than the one the app already holds, and does not
|
|
96
|
+
* replace it.
|
|
97
|
+
*/
|
|
98
|
+
updatedAt?: number;
|
|
85
99
|
}
|
|
86
100
|
export interface DmsRuntimeConfig extends RuntimeConfig {
|
|
87
101
|
public: PublicRuntimeConfig;
|
|
@@ -220,9 +234,22 @@ export interface DmsPluginRegistrationOptions {
|
|
|
220
234
|
clientOnly?: boolean;
|
|
221
235
|
}
|
|
222
236
|
export type DmsComponentPreloader = () => Promise<unknown>;
|
|
237
|
+
export interface DmsComponentRegistrationOptions {
|
|
238
|
+
/**
|
|
239
|
+
* Keeps the component out of the application's global components: it
|
|
240
|
+
* resolves by name only in the component trees of the backend pages the
|
|
241
|
+
* registering module owns (see `DmsPagePayload.module`), where it takes
|
|
242
|
+
* precedence over a public component of the same name. Defaults to `false`.
|
|
243
|
+
*/
|
|
244
|
+
private?: boolean;
|
|
245
|
+
}
|
|
223
246
|
export interface DmsFrontendSdk {
|
|
224
247
|
options: DmsModuleOptions;
|
|
225
|
-
registerComponent(
|
|
248
|
+
registerComponent(
|
|
249
|
+
name: string,
|
|
250
|
+
component: Component,
|
|
251
|
+
options?: DmsComponentRegistrationOptions,
|
|
252
|
+
): void;
|
|
226
253
|
registerPage(
|
|
227
254
|
name: string,
|
|
228
255
|
component: Component,
|
|
@@ -264,6 +291,11 @@ export interface DmsFrontendModule {
|
|
|
264
291
|
setup(sdk: DmsFrontendSdk): void | Promise<void>;
|
|
265
292
|
}
|
|
266
293
|
export interface DmsFrontendModuleRegistration {
|
|
294
|
+
/**
|
|
295
|
+
* The module's manifest name, which its backend pages carry as their
|
|
296
|
+
* `module`. Without it, the module's private components never resolve.
|
|
297
|
+
*/
|
|
298
|
+
name?: string;
|
|
267
299
|
module: DmsFrontendModule;
|
|
268
300
|
options: DmsModuleOptions;
|
|
269
301
|
}
|
|
@@ -283,6 +315,8 @@ export interface DmsRuntimeHooks {
|
|
|
283
315
|
interface DmsComponentRegistration {
|
|
284
316
|
name: string;
|
|
285
317
|
component: Component;
|
|
318
|
+
/** The registering module, for a private component. */
|
|
319
|
+
module?: string;
|
|
286
320
|
}
|
|
287
321
|
|
|
288
322
|
interface DmsFrontendEntry {
|
|
@@ -305,6 +339,8 @@ interface DmsPluginRegistration {
|
|
|
305
339
|
*/
|
|
306
340
|
interface DmsFrontendRegistry {
|
|
307
341
|
components: Map<string, DmsComponentRegistration>;
|
|
342
|
+
/** Private components, by registering module, then by normalized name. */
|
|
343
|
+
privateComponents: Map<string, Map<string, DmsComponentRegistration>>;
|
|
308
344
|
pages: Map<string, DmsFrontendEntry>;
|
|
309
345
|
dynamicPages: Map<string, DmsFrontendEntry>;
|
|
310
346
|
layouts: Map<string, DmsFrontendEntry>;
|
|
@@ -333,6 +369,8 @@ export interface DmsFrontendRuntime {
|
|
|
333
369
|
pendingNavigation?: Promise<void>;
|
|
334
370
|
hasNavigationListener: boolean;
|
|
335
371
|
pageVersion: number;
|
|
372
|
+
/** Whether the page `<Suspense>` waits on the page being navigated to. */
|
|
373
|
+
pageLoading: Ref<boolean>;
|
|
336
374
|
/**
|
|
337
375
|
* Identifies the page on screen (its path, or the error page shown at that
|
|
338
376
|
* path), whose meta `route.meta` holds.
|
|
@@ -351,6 +389,7 @@ export interface DmsFrontendRuntime {
|
|
|
351
389
|
function createDmsFrontendRegistry(): DmsFrontendRegistry {
|
|
352
390
|
return {
|
|
353
391
|
components: new Map(),
|
|
392
|
+
privateComponents: new Map(),
|
|
354
393
|
pages: new Map(),
|
|
355
394
|
dynamicPages: new Map(),
|
|
356
395
|
layouts: new Map(),
|
|
@@ -403,6 +442,7 @@ export function createDmsFrontendRuntime(
|
|
|
403
442
|
}),
|
|
404
443
|
hasNavigationListener: false,
|
|
405
444
|
pageVersion: 0,
|
|
445
|
+
pageLoading: ref(false),
|
|
406
446
|
scope: effectScope(true),
|
|
407
447
|
};
|
|
408
448
|
}
|
|
@@ -1187,23 +1227,82 @@ export function useUserSession<
|
|
|
1187
1227
|
};
|
|
1188
1228
|
}
|
|
1189
1229
|
|
|
1230
|
+
/**
|
|
1231
|
+
* Whether the page loaded by the current navigation is still pending: its
|
|
1232
|
+
* chunk, or an async `setup` it awaits, has not resolved yet. A layout reads
|
|
1233
|
+
* it to show its own placeholder in place of the page. Always `false` on the
|
|
1234
|
+
* server and during hydration, which render the page resolved.
|
|
1235
|
+
*/
|
|
1236
|
+
export function useDmsPageLoading(): Readonly<Ref<boolean>> {
|
|
1237
|
+
return readonly(useDmsRuntime().pageLoading);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/**
|
|
1241
|
+
* The props of the `<Suspense>` around the page that keep `useDmsPageLoading`
|
|
1242
|
+
* in step with it. Cleared before the boundary unmounts, so a page left while
|
|
1243
|
+
* pending never leaves the state set.
|
|
1244
|
+
*/
|
|
1245
|
+
export function dmsPageSuspenseProps(): Record<string, () => void> {
|
|
1246
|
+
const loading = useDmsRuntime().pageLoading;
|
|
1247
|
+
const settle = () => {
|
|
1248
|
+
loading.value = false;
|
|
1249
|
+
};
|
|
1250
|
+
return {
|
|
1251
|
+
onPending: () => {
|
|
1252
|
+
loading.value = true;
|
|
1253
|
+
},
|
|
1254
|
+
onResolve: settle,
|
|
1255
|
+
onVnodeBeforeUnmount: settle,
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1190
1259
|
function registerDmsComponentEntry(
|
|
1191
1260
|
entries: Map<string, DmsComponentRegistration>,
|
|
1192
|
-
|
|
1193
|
-
component: Component,
|
|
1261
|
+
registration: DmsComponentRegistration,
|
|
1194
1262
|
): void {
|
|
1195
|
-
const key = normalizeDmsName(name);
|
|
1196
|
-
if (!entries.has(key)) entries.set(key,
|
|
1263
|
+
const key = normalizeDmsName(registration.name);
|
|
1264
|
+
if (!entries.has(key)) entries.set(key, registration);
|
|
1197
1265
|
}
|
|
1198
1266
|
/** Registers a component unless a higher-priority module already owns its name. */
|
|
1199
1267
|
export function registerDmsComponent(name: string, component: Component): void {
|
|
1200
|
-
registerDmsComponentEntry(registry.components, name, component);
|
|
1268
|
+
registerDmsComponentEntry(registry.components, { name, component });
|
|
1201
1269
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1270
|
+
/**
|
|
1271
|
+
* Resolves a registered component by name. `owner` is the `module` of the
|
|
1272
|
+
* backend page whose component tree is rendered: the private components that
|
|
1273
|
+
* module registered resolve there, ahead of a public one of the same name.
|
|
1274
|
+
* Without it, only public components resolve.
|
|
1275
|
+
*/
|
|
1276
|
+
export function resolveDmsComponent(
|
|
1277
|
+
name: string,
|
|
1278
|
+
owner?: string,
|
|
1279
|
+
): Component | undefined {
|
|
1280
|
+
const key = normalizeDmsName(name);
|
|
1281
|
+
const own = owner
|
|
1282
|
+
? registry.privateComponents.get(owner)?.get(key)
|
|
1283
|
+
: undefined;
|
|
1284
|
+
return (own ?? registry.components.get(key))?.component;
|
|
1204
1285
|
}
|
|
1205
1286
|
export const getDmsComponent = resolveDmsComponent;
|
|
1206
1287
|
|
|
1288
|
+
const PRIVATE_COMPONENT_SEPARATOR = "::";
|
|
1289
|
+
|
|
1290
|
+
function registeredComponents(): DmsComponentRegistration[] {
|
|
1291
|
+
return [
|
|
1292
|
+
...registry.components.values(),
|
|
1293
|
+
...[...registry.privateComponents.values()].flatMap((entries) => [
|
|
1294
|
+
...entries.values(),
|
|
1295
|
+
]),
|
|
1296
|
+
];
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
/** Names a registered component across the server render and hydration. */
|
|
1300
|
+
function componentReference({ name, module }: DmsComponentRegistration) {
|
|
1301
|
+
return module === undefined
|
|
1302
|
+
? name
|
|
1303
|
+
: `${module}${PRIVATE_COMPONENT_SEPARATOR}${name}`;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1207
1306
|
/**
|
|
1208
1307
|
* Records, by registered name, every registered async component a server
|
|
1209
1308
|
* render reaches, so the client can resolve the same ones before hydrating.
|
|
@@ -1213,9 +1312,9 @@ export const getDmsComponent = resolveDmsComponent;
|
|
|
1213
1312
|
*/
|
|
1214
1313
|
export function trackDmsAsyncComponents(app: App): Set<string> {
|
|
1215
1314
|
const names = new Map<Component, string>();
|
|
1216
|
-
|
|
1217
|
-
if ((component as DmsAsyncComponent).__asyncLoader)
|
|
1218
|
-
names.set(component,
|
|
1315
|
+
registeredComponents().forEach((registration) => {
|
|
1316
|
+
if ((registration.component as DmsAsyncComponent).__asyncLoader)
|
|
1317
|
+
names.set(registration.component, componentReference(registration));
|
|
1219
1318
|
});
|
|
1220
1319
|
const rendered = new Set<string>();
|
|
1221
1320
|
app.mixin({
|
|
@@ -1258,10 +1357,18 @@ export function trackDmsServerScopes(app: App): () => void {
|
|
|
1258
1357
|
export async function resolveDmsAsyncComponents(
|
|
1259
1358
|
names: readonly string[],
|
|
1260
1359
|
): Promise<void> {
|
|
1360
|
+
const components = new Map(
|
|
1361
|
+
registeredComponents().map((registration) => [
|
|
1362
|
+
componentReference(registration),
|
|
1363
|
+
registration.component,
|
|
1364
|
+
]),
|
|
1365
|
+
);
|
|
1261
1366
|
await Promise.all(
|
|
1262
1367
|
names.map((name) =>
|
|
1263
1368
|
(
|
|
1264
|
-
resolveDmsComponent(name) as
|
|
1369
|
+
(components.get(name) ?? resolveDmsComponent(name)) as
|
|
1370
|
+
| DmsAsyncComponent
|
|
1371
|
+
| undefined
|
|
1265
1372
|
)?.__asyncLoader?.(),
|
|
1266
1373
|
),
|
|
1267
1374
|
);
|
|
@@ -1303,6 +1410,7 @@ function collectRenderedComponentNames(
|
|
|
1303
1410
|
|
|
1304
1411
|
async function preloadDmsLayoutComponents(
|
|
1305
1412
|
layout: DmsPageLayout | undefined,
|
|
1413
|
+
owner: string | undefined,
|
|
1306
1414
|
): Promise<void> {
|
|
1307
1415
|
const names = new Set<string>();
|
|
1308
1416
|
collectRenderedComponentNames(layout?.layout, names);
|
|
@@ -1311,7 +1419,7 @@ async function preloadDmsLayoutComponents(
|
|
|
1311
1419
|
});
|
|
1312
1420
|
await Promise.all(
|
|
1313
1421
|
[...names].map((name) => {
|
|
1314
|
-
const component = resolveDmsComponent(name) as
|
|
1422
|
+
const component = resolveDmsComponent(name, owner) as
|
|
1315
1423
|
| DmsAsyncComponent
|
|
1316
1424
|
| undefined;
|
|
1317
1425
|
return component?.__asyncLoader?.();
|
|
@@ -1371,6 +1479,24 @@ export function getDmsLayoutProps(
|
|
|
1371
1479
|
};
|
|
1372
1480
|
}
|
|
1373
1481
|
|
|
1482
|
+
/**
|
|
1483
|
+
* Whether a page carries an older session of the same account than the one
|
|
1484
|
+
* the app holds: Inertia restores a page from history, or serves it from the
|
|
1485
|
+
* prefetch cache, with the props it was fetched with, and those must not undo
|
|
1486
|
+
* a later `useUserSession().fetch()`.
|
|
1487
|
+
*/
|
|
1488
|
+
function isStaleSession(
|
|
1489
|
+
incoming: DmsSession | undefined,
|
|
1490
|
+
current: DmsSession | null,
|
|
1491
|
+
): boolean {
|
|
1492
|
+
return (
|
|
1493
|
+
!!incoming &&
|
|
1494
|
+
!!current &&
|
|
1495
|
+
incoming.accountId === current.accountId &&
|
|
1496
|
+
(incoming.updatedAt ?? 0) < (current.updatedAt ?? 0)
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1374
1500
|
export function hydrateDmsPageProps(props: DmsPageProps, url?: string): void {
|
|
1375
1501
|
const runtime = useDmsRuntime();
|
|
1376
1502
|
if (url) updateRoute(runtime, url);
|
|
@@ -1384,10 +1510,12 @@ export function hydrateDmsPageProps(props: DmsPageProps, url?: string): void {
|
|
|
1384
1510
|
}
|
|
1385
1511
|
runtime.pageVersion++;
|
|
1386
1512
|
runtime.currentError.value = null;
|
|
1387
|
-
useDmsState<
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1513
|
+
const session = useDmsState<DmsSession | null>("dms-session", () => null);
|
|
1514
|
+
if (!isStaleSession(props.session, session.value)) {
|
|
1515
|
+
useDmsState<DmsUser | null>("dms-user", () => null).value =
|
|
1516
|
+
props.user ?? null;
|
|
1517
|
+
session.value = props.session ?? null;
|
|
1518
|
+
}
|
|
1391
1519
|
const shared = props.page.shared;
|
|
1392
1520
|
if (shared) {
|
|
1393
1521
|
useDmsState<unknown>("dms-siteLayout", () => undefined).value =
|
|
@@ -1433,7 +1561,8 @@ export async function preloadDmsPage(props: DmsPageProps): Promise<void> {
|
|
|
1433
1561
|
(entry?.component as DmsAsyncComponent | undefined)?.__asyncLoader?.(),
|
|
1434
1562
|
]),
|
|
1435
1563
|
);
|
|
1436
|
-
if (!props.error)
|
|
1564
|
+
if (!props.error)
|
|
1565
|
+
await preloadDmsLayoutComponents(props.page.layout, props.page.module);
|
|
1437
1566
|
}
|
|
1438
1567
|
|
|
1439
1568
|
export function useDmsInjection<T>(
|
|
@@ -1451,14 +1580,29 @@ function registerDmsFrontendEntry(
|
|
|
1451
1580
|
if (!entries.has(name)) entries.set(name, { component, preload });
|
|
1452
1581
|
}
|
|
1453
1582
|
|
|
1583
|
+
function privateComponentsOf(
|
|
1584
|
+
target: DmsFrontendRegistry,
|
|
1585
|
+
owner: string,
|
|
1586
|
+
): Map<string, DmsComponentRegistration> {
|
|
1587
|
+
const entries = target.privateComponents.get(owner) ?? new Map();
|
|
1588
|
+
target.privateComponents.set(owner, entries);
|
|
1589
|
+
return entries;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1454
1592
|
function createSdk(
|
|
1455
1593
|
target: DmsFrontendRegistry,
|
|
1456
|
-
options:
|
|
1594
|
+
{ name: owner = "", options }: DmsFrontendModuleRegistration,
|
|
1457
1595
|
): DmsFrontendSdk {
|
|
1458
1596
|
return {
|
|
1459
1597
|
options,
|
|
1460
|
-
registerComponent: (name, component) =>
|
|
1461
|
-
|
|
1598
|
+
registerComponent: (name, component, registration = {}) =>
|
|
1599
|
+
registration.private
|
|
1600
|
+
? registerDmsComponentEntry(privateComponentsOf(target, owner), {
|
|
1601
|
+
name,
|
|
1602
|
+
component,
|
|
1603
|
+
module: owner,
|
|
1604
|
+
})
|
|
1605
|
+
: registerDmsComponentEntry(target.components, { name, component }),
|
|
1462
1606
|
registerPage: (name, component, preload) => {
|
|
1463
1607
|
const key = normalizeDmsPageKey(name);
|
|
1464
1608
|
registerDmsFrontendEntry(target.pages, key, component, preload);
|
|
@@ -1519,7 +1663,7 @@ export async function setupFrontendModules(
|
|
|
1519
1663
|
runtimeConfig.value.public,
|
|
1520
1664
|
registration.options.public,
|
|
1521
1665
|
);
|
|
1522
|
-
await registration.module.setup(createSdk(next, registration
|
|
1666
|
+
await registration.module.setup(createSdk(next, registration));
|
|
1523
1667
|
}
|
|
1524
1668
|
registry = next;
|
|
1525
1669
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addCollection,
|
|
3
|
+
addIcon,
|
|
4
|
+
getIcon,
|
|
5
|
+
Icon,
|
|
6
|
+
type IconifyIcon,
|
|
7
|
+
type IconifyJSON,
|
|
8
|
+
iconLoaded,
|
|
9
|
+
} from "@iconify/vue";
|
|
10
|
+
import type { App, Component, ComponentPublicInstance } from "vue";
|
|
11
|
+
|
|
12
|
+
/** The element a server render writes the data of the icons it drew to. */
|
|
13
|
+
export const SSR_ICONS_ID = "dms-ssr-icons";
|
|
14
|
+
|
|
15
|
+
interface IconProps {
|
|
16
|
+
icon?: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Draws an Iconify icon in its first render whenever its data is loaded, on
|
|
21
|
+
* the server as in the browser.
|
|
22
|
+
*
|
|
23
|
+
* `@iconify/vue` otherwise renders every icon empty until it mounts, which a
|
|
24
|
+
* server render never does: pages arrived without their icons, which popped in
|
|
25
|
+
* once hydrated. Its `ssr` prop opts out of the wait, but Nuxt UI's `UIcon`
|
|
26
|
+
* does not pass it, so its default is set here, and only for an icon whose
|
|
27
|
+
* data is loaded: an icon still to be fetched waits for the mount as before,
|
|
28
|
+
* so a server render never queries an Iconify API.
|
|
29
|
+
*/
|
|
30
|
+
export function drawLoadedIconsAtOnce(): void {
|
|
31
|
+
(Icon as unknown as { props: Record<string, unknown> }).props.ssr = {
|
|
32
|
+
type: Boolean,
|
|
33
|
+
default: ({ icon }: IconProps) =>
|
|
34
|
+
typeof icon !== "string" || iconLoaded(icon),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Loads whole icon collections, so a server render draws any icon of them,
|
|
40
|
+
* including those only named by backend data, which the client bundle's scan
|
|
41
|
+
* cannot see.
|
|
42
|
+
*/
|
|
43
|
+
export function registerDmsIconCollections(collections: IconifyJSON[]): void {
|
|
44
|
+
for (const collection of collections) addCollection(collection);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Records the name of every Iconify icon a server render reaches. */
|
|
48
|
+
export function trackDmsIcons(app: App): Set<string> {
|
|
49
|
+
const names = new Set<string>();
|
|
50
|
+
app.mixin({
|
|
51
|
+
beforeCreate(this: ComponentPublicInstance) {
|
|
52
|
+
if (this.$.type !== (Icon as Component)) return;
|
|
53
|
+
const { icon } = this.$props as IconProps;
|
|
54
|
+
if (typeof icon === "string") names.add(icon);
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
return names;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The data of the icons a server render drew, which the browser loads before
|
|
62
|
+
* it hydrates: an icon outside the client bundle would otherwise hydrate
|
|
63
|
+
* empty over the server's markup.
|
|
64
|
+
*/
|
|
65
|
+
export function serializeDmsIcons(
|
|
66
|
+
names: Iterable<string>,
|
|
67
|
+
): Record<string, IconifyIcon> {
|
|
68
|
+
return Object.fromEntries(
|
|
69
|
+
[...names].flatMap((name) => {
|
|
70
|
+
const data = getIcon(name);
|
|
71
|
+
return data ? [[name, data]] : [];
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function addDmsIcons(icons: Record<string, IconifyIcon>): void {
|
|
77
|
+
for (const [name, data] of Object.entries(icons)) addIcon(name, data);
|
|
78
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { clientIp } from "./client-ip.mjs";
|
|
1
|
+
import { clientCountryHeader, clientIp } from "./client-ip.mjs";
|
|
2
2
|
|
|
3
3
|
const FORWARDED_HEADERS = [
|
|
4
4
|
"user-agent",
|
|
@@ -26,12 +26,26 @@ export function publicBackendMessage(data) {
|
|
|
26
26
|
: undefined;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
/**
|
|
30
|
+
* The headers of a client request the server relays to the backend.
|
|
31
|
+
*
|
|
32
|
+
* @param request The client request
|
|
33
|
+
* @param names Lowercase names of the request headers to copy when present
|
|
34
|
+
* @returns The client country header, then the copied headers
|
|
35
|
+
*/
|
|
36
|
+
export function relayedHeaders(request, names) {
|
|
37
|
+
return {
|
|
38
|
+
...clientCountryHeader(request),
|
|
39
|
+
...Object.fromEntries(
|
|
40
|
+
names.flatMap((name) =>
|
|
41
|
+
request.headers[name] ? [[name, request.headers[name]]] : [],
|
|
42
|
+
),
|
|
33
43
|
),
|
|
34
|
-
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function backend(path, request, options = {}) {
|
|
48
|
+
const headers = relayedHeaders(request, FORWARDED_HEADERS);
|
|
35
49
|
headers["x-forwarded-for"] = clientIp(request);
|
|
36
50
|
if (options.token) headers.authorization = `Bearer ${options.token}`;
|
|
37
51
|
if (options.relay)
|
|
@@ -51,6 +51,34 @@ export function clientIp(request) {
|
|
|
51
51
|
return chain[Math.max(0, chain.length - hops - 1)] ?? socketIp;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
const DEFAULT_COUNTRY_HEADER = "CF-IPCountry";
|
|
55
|
+
const HEADER_NAME = /^[!#$%&'*+.^_`|~0-9a-z-]+$/;
|
|
56
|
+
|
|
57
|
+
function countryHeader() {
|
|
58
|
+
const name = (process.env.DMS_COUNTRY_HEADER ?? DEFAULT_COUNTRY_HEADER)
|
|
59
|
+
.trim()
|
|
60
|
+
.toLowerCase();
|
|
61
|
+
return HEADER_NAME.test(name) ? name : undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The client's country header, as the backend request headers to add.
|
|
66
|
+
*
|
|
67
|
+
* The header is the one `DMS_COUNTRY_HEADER` names (`CF-IPCountry` by
|
|
68
|
+
* default, empty to relay none), the name the DMS reads from
|
|
69
|
+
* `auth.signInCountry.header`. Like `X-Forwarded-For`, it is only believed
|
|
70
|
+
* from a trusted reverse proxy: with `DMS_TRUSTED_PROXY_HOPS` at `0`, any
|
|
71
|
+
* caller could have set it, so nothing is relayed.
|
|
72
|
+
*/
|
|
73
|
+
export function clientCountryHeader(request) {
|
|
74
|
+
const name = countryHeader();
|
|
75
|
+
if (!name || !trustedHops()) return {};
|
|
76
|
+
const value = request.headers?.[name];
|
|
77
|
+
return typeof value === "string" && value.trim()
|
|
78
|
+
? { [name]: value.trim() }
|
|
79
|
+
: {};
|
|
80
|
+
}
|
|
81
|
+
|
|
54
82
|
/**
|
|
55
83
|
* Body answered with the 403 of the CSRF origin check.
|
|
56
84
|
*
|
|
@@ -146,9 +146,15 @@ export function assertAccountInvariant(session, accountId) {
|
|
|
146
146
|
throw new Error("Account session invariant violated");
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Write `session` as the current one, and as one of the stored accounts.
|
|
151
|
+
* Stamps it with `updatedAt`, which lets the browser tell a page fetched
|
|
152
|
+
* before this write, and restored from its cache, from a current one.
|
|
153
|
+
*/
|
|
149
154
|
export function persistAccountSession(request, response, session, accountId) {
|
|
150
155
|
const id = accountId ?? session.accountId ?? randomUUID();
|
|
151
156
|
session.accountId = id;
|
|
157
|
+
session.updatedAt = Date.now();
|
|
152
158
|
const account = storeAccount(request, response, session, id);
|
|
153
159
|
assertAccountInvariant(session, account.accountId);
|
|
154
160
|
writeSession(response, session);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -14,12 +14,16 @@ const READ_METHODS = new Set(["GET", "HEAD"]);
|
|
|
14
14
|
const ICON_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
15
15
|
const COLLECTION_FILE = /^([^/]+)\.json$/;
|
|
16
16
|
const ICON_NAME_SEPARATOR = ",";
|
|
17
|
+
const COLLECTION_PACKAGES = join("node_modules", "@iconify-json");
|
|
17
18
|
// An icon's data only changes with the installed collection, which a
|
|
18
19
|
// deployment replaces; a week bounds how long an upgraded icon can lag.
|
|
19
20
|
const ICON_CACHE_CONTROL =
|
|
20
21
|
"public, max-age=604800, stale-while-revalidate=86400";
|
|
21
22
|
const ERROR_CACHE_CONTROL = "no-store";
|
|
22
23
|
const collections = new Map();
|
|
24
|
+
// By renderer module: a development server evaluates a new one after a layer
|
|
25
|
+
// changes, and each must find the collections loaded before it renders.
|
|
26
|
+
const rendererCollections = new WeakMap();
|
|
23
27
|
|
|
24
28
|
function collectionSpecifiers(prefix) {
|
|
25
29
|
return [
|
|
@@ -60,6 +64,44 @@ function collection(root, prefix) {
|
|
|
60
64
|
return collections.get(key);
|
|
61
65
|
}
|
|
62
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Every `@iconify-json/<prefix>` collection installed in the workspace under
|
|
69
|
+
* `root`, the ones the server render draws from. The workspace hoists every
|
|
70
|
+
* dependency, a module's collections included.
|
|
71
|
+
*/
|
|
72
|
+
export async function installedCollections(root) {
|
|
73
|
+
const prefixes = await readdir(join(root, COLLECTION_PACKAGES)).catch(
|
|
74
|
+
() => [],
|
|
75
|
+
);
|
|
76
|
+
const loaded = await Promise.all(
|
|
77
|
+
prefixes
|
|
78
|
+
.filter((prefix) => ICON_NAME.test(prefix))
|
|
79
|
+
.map((prefix) => collection(root, prefix)),
|
|
80
|
+
);
|
|
81
|
+
return loaded.filter(Boolean);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Hand a server renderer the installed collections, once per renderer module,
|
|
86
|
+
* before its first render. A collection that fails to load leaves the render
|
|
87
|
+
* with the bundled icons only.
|
|
88
|
+
*/
|
|
89
|
+
export function provideIconCollections(renderer, root = WORKSPACE_ROOT) {
|
|
90
|
+
if (!rendererCollections.has(renderer))
|
|
91
|
+
rendererCollections.set(
|
|
92
|
+
renderer,
|
|
93
|
+
installedCollections(root)
|
|
94
|
+
.then((loaded) => renderer.registerDmsIconCollections(loaded))
|
|
95
|
+
.catch((error) =>
|
|
96
|
+
console.error(
|
|
97
|
+
"DMS icon collections not loaded; server renders draw bundled icons only",
|
|
98
|
+
error,
|
|
99
|
+
),
|
|
100
|
+
),
|
|
101
|
+
);
|
|
102
|
+
return rendererCollections.get(renderer);
|
|
103
|
+
}
|
|
104
|
+
|
|
63
105
|
function jsonResponse(status, cacheControl, payload) {
|
|
64
106
|
return {
|
|
65
107
|
status,
|
package/templates/vue/server.mjs
CHANGED
|
@@ -5,7 +5,11 @@ import { Readable } from "node:stream";
|
|
|
5
5
|
import { pipeline } from "node:stream/promises";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { ofetch } from "ofetch";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
relayedHeaders,
|
|
10
|
+
RequestBodyError,
|
|
11
|
+
UpstreamError,
|
|
12
|
+
} from "./server/auth/backend.mjs";
|
|
9
13
|
import { CROSS_ORIGIN_ERROR, isSameOrigin } from "./server/auth/client-ip.mjs";
|
|
10
14
|
import {
|
|
11
15
|
handleAuth,
|
|
@@ -36,7 +40,7 @@ import { documentStyleTags } from "./server/dev-styles.mjs";
|
|
|
36
40
|
import { handleEmailRender, watchEmailBundle } from "./server/email.mjs";
|
|
37
41
|
import { HOMEPAGE } from "./server/homepage.mjs";
|
|
38
42
|
import { htmlTag } from "./server/html-tag.mjs";
|
|
39
|
-
import
|
|
43
|
+
import * as icons from "./server/icons.mjs";
|
|
40
44
|
import * as requestFailure from "./server/request-failure.mjs";
|
|
41
45
|
import { handleTester } from "./server/tester.mjs";
|
|
42
46
|
import {
|
|
@@ -75,7 +79,7 @@ const HTML_RENDER_ROUTE = "/api/html/render";
|
|
|
75
79
|
const TESTER_ROUTE = /^\/api\/_dms\/tester\/?$/;
|
|
76
80
|
const LOCAL_ROUTES = [
|
|
77
81
|
[(pathname) => TESTER_ROUTE.test(pathname), handleTester],
|
|
78
|
-
[isIconRequest, handleIcons],
|
|
82
|
+
[icons.isIconRequest, icons.handleIcons],
|
|
79
83
|
];
|
|
80
84
|
const AUTH_PAGE = "/auth";
|
|
81
85
|
const ONBOARDING_PAGE = "/onboarding";
|
|
@@ -117,18 +121,13 @@ async function developmentServer() {
|
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
function backendHeaders(request) {
|
|
120
|
-
const
|
|
124
|
+
const headers = relayedHeaders(request, [
|
|
121
125
|
"accept",
|
|
122
126
|
"content-type",
|
|
123
127
|
"user-agent",
|
|
124
128
|
"x-content-language",
|
|
125
129
|
"x-realtime-session",
|
|
126
|
-
];
|
|
127
|
-
const headers = Object.fromEntries(
|
|
128
|
-
names.flatMap((name) =>
|
|
129
|
-
request.headers[name] ? [[name, request.headers[name]]] : [],
|
|
130
|
-
),
|
|
131
|
-
);
|
|
130
|
+
]);
|
|
132
131
|
const session = readSession(request);
|
|
133
132
|
if (session?.accessToken)
|
|
134
133
|
headers.authorization = `Bearer ${session.accessToken}`;
|
|
@@ -297,9 +296,11 @@ function serveAsset(pathname, request, response) {
|
|
|
297
296
|
}
|
|
298
297
|
|
|
299
298
|
async function ssrRenderer(devServer) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
299
|
+
const renderer = devServer
|
|
300
|
+
? await devServer.ssrLoadModule("/ssr-renderer.ts")
|
|
301
|
+
: await (productionSsrRenderer ??= import(BUILT_SSR_RENDERER_PATH));
|
|
302
|
+
await icons.provideIconCollections(renderer);
|
|
303
|
+
return renderer;
|
|
303
304
|
}
|
|
304
305
|
|
|
305
306
|
export async function renderHtml(
|