@antelopejs/dms-frontend 0.2.7 → 0.2.8
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 +11 -0
- package/dist/config.js +1 -0
- package/dist/workspace-setup.d.ts +1 -1
- package/dist/workspace-setup.js +18 -3
- package/package.json +1 -1
- package/templates/vue/app-runtime.ts +20 -1
- package/templates/vue/frontend-module.ts +132 -27
- package/templates/vue/package.json +1 -1
- package/templates/vue/patches/@nuxt__ui@4.10.0.patch +44 -0
- package/templates/vue/pnpm-workspace.yaml +7 -0
- package/templates/vue/server/html-tag.mjs +15 -0
- package/templates/vue/server.mjs +18 -13
- package/templates/vue/ssr-renderer.ts +13 -1
- package/templates/vue/vite.config.ts +1 -1
package/README.md
CHANGED
|
@@ -133,6 +133,17 @@ export default frontendModule;
|
|
|
133
133
|
|
|
134
134
|
The SDK also exposes `use` for Vue plugins. Entries execute by descending manifest priority, then stable module id. Modules without `dms.frontend.ts` are skipped by the generated loader. Registered page keys match a route's `fullSlug`, request path, or `default`; unregistered pages and components use the generic card renderer.
|
|
135
135
|
|
|
136
|
+
### Components rendered inside `<svg>`
|
|
137
|
+
|
|
138
|
+
Modules usually register their components lazily, with `defineAsyncComponent`, and the page is rendered under a `<Suspense>`. Vue mounts an async component that resolves under a `<Suspense>` with the `<Suspense>`'s element namespace rather than the namespace of the place it is rendered: a component resolved by name (`resolveComponent` or a template tag) whose root is an SVG element (`<path>`, `<g>`, …) is created as an HTML element inside the `<svg>`, and the browser draws nothing ([vuejs/core#15639](https://github.com/vuejs/core/issues/15639)). Import such components directly instead, for example the node and edge types passed to Vue Flow:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { markRaw } from "vue";
|
|
142
|
+
import RelationEdge from "./RelationEdge.vue";
|
|
143
|
+
|
|
144
|
+
const edgeTypes = markRaw({ relation: RelationEdge });
|
|
145
|
+
```
|
|
146
|
+
|
|
136
147
|
### Layer aliases outside the workspace
|
|
137
148
|
|
|
138
149
|
Every layer a module ships under `layers/<name>/` answers to `#<name>` inside
|
package/dist/config.js
CHANGED
|
@@ -52,6 +52,7 @@ exports.TEMPLATE_FILES = [
|
|
|
52
52
|
["npmrc", ".npmrc"],
|
|
53
53
|
["gitignore", ".gitignore"],
|
|
54
54
|
["pnpm-workspace.yaml", "pnpm-workspace.yaml"],
|
|
55
|
+
["patches", "patches"],
|
|
55
56
|
];
|
|
56
57
|
exports.LAYERS_SUBDIR = "frontend-modules";
|
|
57
58
|
exports.FRONTEND_MODULE_ENTRY = "dms.frontend.ts";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type SpawnOptions } from "node:child_process";
|
|
2
2
|
import { ResolvedLayer } from "./workspace";
|
|
3
|
-
export declare function computeDepsHash(layerPaths: string[], workspacePackage?: string): string;
|
|
3
|
+
export declare function computeDepsHash(layerPaths: string[], workspacePackage?: string, installInputs?: readonly string[]): string;
|
|
4
4
|
export declare function needsInstall(workspaceDir: string, layerPaths: string[], force: boolean): boolean;
|
|
5
5
|
export declare function saveDepsHash(workspaceDir: string, layerPaths: string[]): void;
|
|
6
6
|
export declare function installDeps(workspaceDir: string): void;
|
package/dist/workspace-setup.js
CHANGED
|
@@ -16,10 +16,25 @@ const manifest_1 = require("./manifest");
|
|
|
16
16
|
const materialize_1 = require("./materialize");
|
|
17
17
|
const workspace_1 = require("./workspace");
|
|
18
18
|
const config_1 = require("./config");
|
|
19
|
-
|
|
19
|
+
const WORKSPACE_PNPM_CONFIG = "pnpm-workspace.yaml";
|
|
20
|
+
const WORKSPACE_PATCHES_DIR = "patches";
|
|
21
|
+
function workspaceInstallInputs(workspaceDir) {
|
|
22
|
+
const patchesDir = (0, node_path_1.join)(workspaceDir, WORKSPACE_PATCHES_DIR);
|
|
23
|
+
const patches = (0, node_fs_1.existsSync)(patchesDir)
|
|
24
|
+
? (0, node_fs_1.readdirSync)(patchesDir)
|
|
25
|
+
.sort()
|
|
26
|
+
.map((name) => (0, node_path_1.join)(patchesDir, name))
|
|
27
|
+
: [];
|
|
28
|
+
return [(0, node_path_1.join)(workspaceDir, WORKSPACE_PNPM_CONFIG), ...patches];
|
|
29
|
+
}
|
|
30
|
+
function computeDepsHash(layerPaths, workspacePackage, installInputs = []) {
|
|
20
31
|
const hash = (0, node_crypto_1.createHash)("sha256");
|
|
21
32
|
if (workspacePackage && (0, node_fs_1.existsSync)(workspacePackage))
|
|
22
33
|
hash.update((0, node_fs_1.readFileSync)(workspacePackage));
|
|
34
|
+
for (const input of installInputs) {
|
|
35
|
+
if ((0, node_fs_1.existsSync)(input))
|
|
36
|
+
hash.update((0, node_fs_1.readFileSync)(input));
|
|
37
|
+
}
|
|
23
38
|
for (const layerPath of [...layerPaths].sort()) {
|
|
24
39
|
const pkgPath = (0, node_path_1.join)(layerPath, "package.json");
|
|
25
40
|
if ((0, node_fs_1.existsSync)(pkgPath)) {
|
|
@@ -36,12 +51,12 @@ function needsInstall(workspaceDir, layerPaths, force) {
|
|
|
36
51
|
return true;
|
|
37
52
|
if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(workspaceDir, "node_modules")))
|
|
38
53
|
return true;
|
|
39
|
-
const currentHash = computeDepsHash(layerPaths, (0, node_path_1.join)(workspaceDir, "package.json"));
|
|
54
|
+
const currentHash = computeDepsHash(layerPaths, (0, node_path_1.join)(workspaceDir, "package.json"), workspaceInstallInputs(workspaceDir));
|
|
40
55
|
const savedHash = (0, node_fs_1.readFileSync)(hashFile, "utf-8").trim();
|
|
41
56
|
return currentHash !== savedHash;
|
|
42
57
|
}
|
|
43
58
|
function saveDepsHash(workspaceDir, layerPaths) {
|
|
44
|
-
const hash = computeDepsHash(layerPaths, (0, node_path_1.join)(workspaceDir, "package.json"));
|
|
59
|
+
const hash = computeDepsHash(layerPaths, (0, node_path_1.join)(workspaceDir, "package.json"), workspaceInstallInputs(workspaceDir));
|
|
45
60
|
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(workspaceDir, config_1.DEPS_HASH_FILE), hash);
|
|
46
61
|
}
|
|
47
62
|
function installDeps(workspaceDir) {
|
package/package.json
CHANGED
|
@@ -31,8 +31,10 @@ import {
|
|
|
31
31
|
installDmsPlugins,
|
|
32
32
|
preloadDmsPage,
|
|
33
33
|
provideDmsFrontendRuntime,
|
|
34
|
+
resolveDmsAsyncComponents,
|
|
34
35
|
resolveDmsComponent,
|
|
35
36
|
serializeDmsAsyncData,
|
|
37
|
+
trackDmsAsyncComponents,
|
|
36
38
|
useDmsAppConfig,
|
|
37
39
|
useDmsState,
|
|
38
40
|
useError,
|
|
@@ -72,6 +74,7 @@ export type DmsServerFetch = (
|
|
|
72
74
|
) => Promise<unknown>;
|
|
73
75
|
|
|
74
76
|
const SSR_ASYNC_DATA_ID = "dms-ssr-async-data";
|
|
77
|
+
export const SSR_ASYNC_COMPONENTS_ID = "dms-ssr-async-components";
|
|
75
78
|
const DEFAULT_LOCALE = "en";
|
|
76
79
|
|
|
77
80
|
function pageLocale(props: DmsPageProps): string {
|
|
@@ -90,6 +93,15 @@ function readDmsAsyncData(): Record<string, unknown> {
|
|
|
90
93
|
return data;
|
|
91
94
|
}
|
|
92
95
|
|
|
96
|
+
function readDmsAsyncComponents(): string[] {
|
|
97
|
+
if (typeof document === "undefined") return [];
|
|
98
|
+
const element = document.getElementById(SSR_ASYNC_COMPONENTS_ID);
|
|
99
|
+
if (!element?.textContent) return [];
|
|
100
|
+
const names = JSON.parse(element.textContent) as string[];
|
|
101
|
+
element.remove();
|
|
102
|
+
return names;
|
|
103
|
+
}
|
|
104
|
+
|
|
93
105
|
const DmsInertiaPage = defineComponent({
|
|
94
106
|
name: "DmsInertiaPage",
|
|
95
107
|
inheritAttrs: false,
|
|
@@ -227,6 +239,13 @@ export async function configureDmsApp(
|
|
|
227
239
|
options.app.runWithContext(() =>
|
|
228
240
|
hydrateDmsPageProps(options.initialPageProps, options.initialPageUrl),
|
|
229
241
|
);
|
|
242
|
+
// Resolve, before the app mounts, every async component the server
|
|
243
|
+
// rendered: the page, its layout, and the components the render reached.
|
|
244
|
+
// Hydration then adopts the server-rendered markup instead of discarding it.
|
|
245
|
+
await Promise.all([
|
|
246
|
+
preloadDmsPage(options.initialPageProps),
|
|
247
|
+
resolveDmsAsyncComponents(readDmsAsyncComponents()),
|
|
248
|
+
]);
|
|
230
249
|
const mounted = await installDmsPlugins(
|
|
231
250
|
options.app,
|
|
232
251
|
i18n.global,
|
|
@@ -237,4 +256,4 @@ export async function configureDmsApp(
|
|
|
237
256
|
return { mounted, runtime };
|
|
238
257
|
}
|
|
239
258
|
|
|
240
|
-
export { serializeDmsAsyncData };
|
|
259
|
+
export { serializeDmsAsyncData, trackDmsAsyncComponents };
|
|
@@ -13,6 +13,7 @@ import { type FetchOptions, ofetch } from "ofetch";
|
|
|
13
13
|
import {
|
|
14
14
|
type App,
|
|
15
15
|
type Component,
|
|
16
|
+
type ComponentPublicInstance,
|
|
16
17
|
type ComputedRef,
|
|
17
18
|
computed,
|
|
18
19
|
defineComponent,
|
|
@@ -313,6 +314,8 @@ export interface DmsFrontendRuntime {
|
|
|
313
314
|
pendingNavigation?: Promise<void>;
|
|
314
315
|
hasNavigationListener: boolean;
|
|
315
316
|
pageVersion: number;
|
|
317
|
+
/** The `Cookie` header of the request a server runtime renders. */
|
|
318
|
+
requestCookies?: string;
|
|
316
319
|
}
|
|
317
320
|
|
|
318
321
|
const components = new Map<string, DmsComponentRegistration>();
|
|
@@ -349,8 +352,10 @@ export function createDmsFrontendRuntime(
|
|
|
349
352
|
serverFetch?: typeof ofetch,
|
|
350
353
|
hydratedAsyncData: Record<string, unknown> = {},
|
|
351
354
|
isServer = false,
|
|
355
|
+
requestCookies?: string,
|
|
352
356
|
): DmsFrontendRuntime {
|
|
353
357
|
return {
|
|
358
|
+
requestCookies,
|
|
354
359
|
sharedState: new Map(),
|
|
355
360
|
asyncData: new Map(),
|
|
356
361
|
asyncDataPromises: new Map(),
|
|
@@ -519,16 +524,24 @@ async function visit(
|
|
|
519
524
|
options: DmsNavigationOptions = {},
|
|
520
525
|
): Promise<void> {
|
|
521
526
|
const url = locationToUrl(runtime, to);
|
|
522
|
-
const
|
|
527
|
+
const target = parseRoute(runtime, url);
|
|
528
|
+
const result = await runMiddleware(runtime, target);
|
|
523
529
|
if (result === false) return;
|
|
524
530
|
if (result !== undefined) return visit(runtime, result, options);
|
|
525
531
|
if (runtime.isServer) {
|
|
526
532
|
runtime.serverRedirect = url;
|
|
527
533
|
return;
|
|
528
534
|
}
|
|
535
|
+
// Like Vue Router, a navigation that only changes the query or hash keeps
|
|
536
|
+
// the page mounted: without preserveState, Inertia remounts the page
|
|
537
|
+
// component on every visit and its local state (open tabs, selection,
|
|
538
|
+
// inputs) is lost.
|
|
539
|
+
const samePath = target.path === runtime.route.path;
|
|
529
540
|
await new Promise<void>((resolve) =>
|
|
530
541
|
inertiaRouter.visit(url, {
|
|
531
542
|
...options,
|
|
543
|
+
preserveState: options.preserveState ?? samePath,
|
|
544
|
+
preserveScroll: options.preserveScroll ?? samePath,
|
|
532
545
|
onFinish: (completedVisit) => {
|
|
533
546
|
options.onFinish?.(completedVisit);
|
|
534
547
|
resolve();
|
|
@@ -1004,44 +1017,94 @@ export function useDmsApp(): DmsAppContext {
|
|
|
1004
1017
|
return contextual ?? (useDmsRuntime().appContext as DmsAppContext);
|
|
1005
1018
|
}
|
|
1006
1019
|
|
|
1007
|
-
|
|
1020
|
+
const COOKIE_STATE_PREFIX = "dms-cookie:";
|
|
1021
|
+
// A browser tab runs one application, so its cookie refs are shared by every
|
|
1022
|
+
// caller; a server shares them within the request it renders instead.
|
|
1023
|
+
const browserCookieRefs = new Map<string, Ref<unknown>>();
|
|
1024
|
+
|
|
1025
|
+
function readCookie(
|
|
1026
|
+
source: string | undefined,
|
|
1008
1027
|
name: string,
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
return ref(options.default?.() ?? null) as Ref<T>;
|
|
1013
|
-
const match = document.cookie
|
|
1014
|
-
.split("; ")
|
|
1028
|
+
): string | undefined {
|
|
1029
|
+
const match = source
|
|
1030
|
+
?.split(/;\s*/)
|
|
1015
1031
|
.find((entry) => entry.startsWith(`${name}=`));
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1032
|
+
return match ? decodeURIComponent(match.slice(name.length + 1)) : undefined;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function parseCookieValue<T>(
|
|
1036
|
+
stored: string | undefined,
|
|
1037
|
+
options: DmsCookieOptions<T>,
|
|
1038
|
+
): T {
|
|
1039
|
+
if (stored === undefined) return (options.default?.() ?? null) as T;
|
|
1020
1040
|
try {
|
|
1021
|
-
|
|
1022
|
-
stored === undefined
|
|
1023
|
-
? ((options.default?.() ?? null) as T)
|
|
1024
|
-
: JSON.parse(stored);
|
|
1041
|
+
return JSON.parse(stored) as T;
|
|
1025
1042
|
} catch {
|
|
1026
|
-
|
|
1043
|
+
return stored as T;
|
|
1027
1044
|
}
|
|
1028
|
-
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function writeCookie<T>(name: string, next: T, options: DmsCookieOptions<T>) {
|
|
1048
|
+
const attributes = [
|
|
1049
|
+
`path=${options.path ?? "/"}`,
|
|
1050
|
+
options.maxAge === undefined ? "" : `max-age=${options.maxAge}`,
|
|
1051
|
+
options.sameSite ? `samesite=${options.sameSite}` : "",
|
|
1052
|
+
options.secure ? "secure" : "",
|
|
1053
|
+
].filter(Boolean);
|
|
1054
|
+
// biome-ignore lint/suspicious/noDocumentCookie: reactive cookie refs require synchronous writes.
|
|
1055
|
+
document.cookie = `${name}=${encodeURIComponent(JSON.stringify(next ?? null))}; ${attributes.join("; ")}`;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function createCookieRef<T>(
|
|
1059
|
+
name: string,
|
|
1060
|
+
source: string | undefined,
|
|
1061
|
+
options: DmsCookieOptions<T>,
|
|
1062
|
+
isServer: boolean,
|
|
1063
|
+
): Ref<T> {
|
|
1064
|
+
const value = ref(
|
|
1065
|
+
parseCookieValue(readCookie(source, name), options),
|
|
1066
|
+
) as Ref<T>;
|
|
1029
1067
|
return computed({
|
|
1030
1068
|
get: () => value.value,
|
|
1031
1069
|
set: (next) => {
|
|
1032
1070
|
value.value = next;
|
|
1033
|
-
|
|
1034
|
-
`path=${options.path ?? "/"}`,
|
|
1035
|
-
options.maxAge === undefined ? "" : `max-age=${options.maxAge}`,
|
|
1036
|
-
options.sameSite ? `samesite=${options.sameSite}` : "",
|
|
1037
|
-
options.secure ? "secure" : "",
|
|
1038
|
-
].filter(Boolean);
|
|
1039
|
-
// biome-ignore lint/suspicious/noDocumentCookie: reactive cookie refs require synchronous writes.
|
|
1040
|
-
document.cookie = `${name}=${encodeURIComponent(JSON.stringify(next ?? null))}; ${attributes.join("; ")}`;
|
|
1071
|
+
if (!isServer) writeCookie(name, next, options);
|
|
1041
1072
|
},
|
|
1042
1073
|
});
|
|
1043
1074
|
}
|
|
1044
1075
|
|
|
1076
|
+
/**
|
|
1077
|
+
* A cookie as a ref, shared by every caller of the same name — so a plugin and
|
|
1078
|
+
* a page reading one preference see each other's writes — and read on the
|
|
1079
|
+
* server from the request being rendered, so the server HTML matches the client.
|
|
1080
|
+
*/
|
|
1081
|
+
export function useDmsCookie<T = string | null>(
|
|
1082
|
+
name: string,
|
|
1083
|
+
options: DmsCookieOptions<T> = {},
|
|
1084
|
+
): Ref<T> {
|
|
1085
|
+
if (typeof document !== "undefined") {
|
|
1086
|
+
if (!browserCookieRefs.has(name))
|
|
1087
|
+
browserCookieRefs.set(
|
|
1088
|
+
name,
|
|
1089
|
+
createCookieRef(name, document.cookie, options, false) as Ref<unknown>,
|
|
1090
|
+
);
|
|
1091
|
+
return browserCookieRefs.get(name) as Ref<T>;
|
|
1092
|
+
}
|
|
1093
|
+
const runtime = useDmsRuntime();
|
|
1094
|
+
const key = `${COOKIE_STATE_PREFIX}${name}`;
|
|
1095
|
+
if (!runtime.sharedState.has(key))
|
|
1096
|
+
runtime.sharedState.set(
|
|
1097
|
+
key,
|
|
1098
|
+
createCookieRef(
|
|
1099
|
+
name,
|
|
1100
|
+
runtime.requestCookies,
|
|
1101
|
+
options,
|
|
1102
|
+
true,
|
|
1103
|
+
) as Ref<unknown>,
|
|
1104
|
+
);
|
|
1105
|
+
return runtime.sharedState.get(key) as Ref<T>;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1045
1108
|
function preferredColorMode(): "light" | "dark" {
|
|
1046
1109
|
return window.matchMedia?.("(prefers-color-scheme: dark)").matches
|
|
1047
1110
|
? "dark"
|
|
@@ -1141,6 +1204,41 @@ export function resolveDmsComponent(name: string): Component | undefined {
|
|
|
1141
1204
|
return components.get(normalizeDmsName(name))?.component;
|
|
1142
1205
|
}
|
|
1143
1206
|
export const getDmsComponent = resolveDmsComponent;
|
|
1207
|
+
|
|
1208
|
+
/**
|
|
1209
|
+
* Records, by registered name, every registered async component a server
|
|
1210
|
+
* render reaches, so the client can resolve the same ones before hydrating.
|
|
1211
|
+
* Inertia re-renders the tree once on boot; an async component still loading
|
|
1212
|
+
* at that moment makes Vue drop its server-rendered markup ("Skipping lazy
|
|
1213
|
+
* hydration") and leave it empty until its chunk arrives.
|
|
1214
|
+
*/
|
|
1215
|
+
export function trackDmsAsyncComponents(app: App): Set<string> {
|
|
1216
|
+
const names = new Map<Component, string>();
|
|
1217
|
+
components.forEach(({ component, name }) => {
|
|
1218
|
+
if ((component as DmsAsyncComponent).__asyncLoader)
|
|
1219
|
+
names.set(component, name);
|
|
1220
|
+
});
|
|
1221
|
+
const rendered = new Set<string>();
|
|
1222
|
+
app.mixin({
|
|
1223
|
+
beforeCreate(this: ComponentPublicInstance) {
|
|
1224
|
+
const name = names.get(this.$.type as Component);
|
|
1225
|
+
if (name) rendered.add(name);
|
|
1226
|
+
},
|
|
1227
|
+
});
|
|
1228
|
+
return rendered;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
export async function resolveDmsAsyncComponents(
|
|
1232
|
+
names: readonly string[],
|
|
1233
|
+
): Promise<void> {
|
|
1234
|
+
await Promise.all(
|
|
1235
|
+
names.map((name) =>
|
|
1236
|
+
(
|
|
1237
|
+
resolveDmsComponent(name) as DmsAsyncComponent | undefined
|
|
1238
|
+
)?.__asyncLoader?.(),
|
|
1239
|
+
),
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1144
1242
|
export async function preloadComponents(names: string[]): Promise<void> {
|
|
1145
1243
|
const registry = useDmsRuntime().appContext?.vueApp._context.components ?? {};
|
|
1146
1244
|
await Promise.all(
|
|
@@ -1281,8 +1379,15 @@ export async function preloadDmsPage(props: DmsPageProps): Promise<void> {
|
|
|
1281
1379
|
? layouts.get(normalizeDmsName(layoutName))
|
|
1282
1380
|
: undefined;
|
|
1283
1381
|
const entries = props.error ? [errorPage] : [page, layout];
|
|
1382
|
+
// `preload` only warms the module; the async wrapper stays unresolved until
|
|
1383
|
+
// its own loader runs. Hydrating an unresolved wrapper defers it, and the
|
|
1384
|
+
// first parent render (Inertia's initial swap) then makes Vue drop the
|
|
1385
|
+
// server-rendered markup and render the subtree from scratch.
|
|
1284
1386
|
await Promise.all(
|
|
1285
|
-
entries.flatMap((entry) =>
|
|
1387
|
+
entries.flatMap((entry) => [
|
|
1388
|
+
entry?.preload?.(),
|
|
1389
|
+
(entry?.component as DmsAsyncComponent | undefined)?.__asyncLoader?.(),
|
|
1390
|
+
]),
|
|
1286
1391
|
);
|
|
1287
1392
|
if (!props.error) await preloadDmsLayoutComponents(props.page.layout);
|
|
1288
1393
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
diff --git a/dist/runtime/vue/overrides/inertia/Link.vue b/dist/runtime/vue/overrides/inertia/Link.vue
|
|
2
|
+
index 5af1e3e..6cf09e1 100644
|
|
3
|
+
--- a/dist/runtime/vue/overrides/inertia/Link.vue
|
|
4
|
+
+++ b/dist/runtime/vue/overrides/inertia/Link.vue
|
|
5
|
+
@@ -106,20 +106,32 @@ const rel = computed(() => {
|
|
6
|
+
}
|
|
7
|
+
return null;
|
|
8
|
+
});
|
|
9
|
+
+// Patched by @antelopejs/dms-frontend until nuxt/ui#6809 is released: match on
|
|
10
|
+
+// path segments, so `/test` is not active on `/test-a` (nuxt/ui#6803). A `to`
|
|
11
|
+
+// that is not a string (a route object) has no path to match, as before.
|
|
12
|
+
+function normalizePath(url) {
|
|
13
|
+
+ if (typeof url !== "string") {
|
|
14
|
+
+ return void 0;
|
|
15
|
+
+ }
|
|
16
|
+
+ const path = url.split("#")[0].split("?")[0];
|
|
17
|
+
+ if (!path) {
|
|
18
|
+
+ return void 0;
|
|
19
|
+
+ }
|
|
20
|
+
+ return path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
|
|
21
|
+
+}
|
|
22
|
+
+const currentPath = computed(() => normalizePath(page.url) ?? "/");
|
|
23
|
+
+const targetPath = computed(() => href.value ? normalizePath(href.value) : void 0);
|
|
24
|
+
const isLinkActive = computed(() => {
|
|
25
|
+
if (props.active !== void 0) {
|
|
26
|
+
return props.active;
|
|
27
|
+
}
|
|
28
|
+
- if (!href.value) {
|
|
29
|
+
+ if (!targetPath.value) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
- if (props.exact && page.url === href.value) {
|
|
33
|
+
- return true;
|
|
34
|
+
- }
|
|
35
|
+
- if (!props.exact && page.url.startsWith(href.value)) {
|
|
36
|
+
- return true;
|
|
37
|
+
+ if (props.exact) {
|
|
38
|
+
+ return page.url === href.value;
|
|
39
|
+
}
|
|
40
|
+
- return false;
|
|
41
|
+
+ return currentPath.value === targetPath.value || currentPath.value.startsWith(targetPath.value === "/" ? "/" : `${targetPath.value}/`);
|
|
42
|
+
});
|
|
43
|
+
const linkClass = computed(() => {
|
|
44
|
+
const active = isLinkActive.value;
|
|
@@ -2,3 +2,10 @@ packages:
|
|
|
2
2
|
- .
|
|
3
3
|
- frontend-modules/*
|
|
4
4
|
ignore-workspace-root-check: true
|
|
5
|
+
# Fixes carried until they are released upstream. Each patch names the upstream
|
|
6
|
+
# issue and pull request it stands in for; drop it with the upgrade that ships
|
|
7
|
+
# them, since pnpm refuses a patch whose version is no longer installed.
|
|
8
|
+
patchedDependencies:
|
|
9
|
+
# Menu and breadcrumb links active on a sibling route (`/test` on `/test-a`):
|
|
10
|
+
# nuxt/ui#6803, fixed by nuxt/ui#6809.
|
|
11
|
+
"@nuxt/ui@4.10.0": patches/@nuxt__ui@4.10.0.patch
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const HTML_ATTRIBUTE = /([^\s=]+)(?:="[^"]*")?/g;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The document's `<html>` tag: the template's attributes, overridden by those
|
|
5
|
+
* the render produced (`lang`, `data-scale`…), so none appears twice.
|
|
6
|
+
*/
|
|
7
|
+
export function htmlTag(templateAttributes, renderedAttributes) {
|
|
8
|
+
const rendered = new Set(
|
|
9
|
+
[...renderedAttributes.matchAll(HTML_ATTRIBUTE)].map((match) => match[1]),
|
|
10
|
+
);
|
|
11
|
+
const kept = [...templateAttributes.matchAll(HTML_ATTRIBUTE)]
|
|
12
|
+
.filter((match) => !rendered.has(match[1]))
|
|
13
|
+
.map((match) => match[0]);
|
|
14
|
+
return `<html ${[...kept, renderedAttributes.trim()].filter(Boolean).join(" ")}>`;
|
|
15
|
+
}
|
package/templates/vue/server.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import { productionHtmlTemplate } from "./server/client-manifest.mjs";
|
|
|
22
22
|
import { documentStyleTags } from "./server/dev-styles.mjs";
|
|
23
23
|
import { handleEmailRender } from "./server/email.mjs";
|
|
24
24
|
import { HOMEPAGE } from "./server/homepage.mjs";
|
|
25
|
+
import { htmlTag } from "./server/html-tag.mjs";
|
|
25
26
|
import { handleTester } from "./server/tester.mjs";
|
|
26
27
|
import {
|
|
27
28
|
createInertiaPage,
|
|
@@ -319,7 +320,12 @@ async function ssrRenderer(devServer) {
|
|
|
319
320
|
return productionSsrRenderer;
|
|
320
321
|
}
|
|
321
322
|
|
|
322
|
-
export async function renderHtml(
|
|
323
|
+
export async function renderHtml(
|
|
324
|
+
page,
|
|
325
|
+
requestUrl,
|
|
326
|
+
serverFetch,
|
|
327
|
+
requestCookies,
|
|
328
|
+
) {
|
|
323
329
|
const devServer = await developmentServer();
|
|
324
330
|
const rawTemplate = devServer
|
|
325
331
|
? readFileSync(SOURCE_TEMPLATE_PATH, "utf8")
|
|
@@ -330,7 +336,7 @@ export async function renderHtml(page, requestUrl, serverFetch) {
|
|
|
330
336
|
let rendered;
|
|
331
337
|
try {
|
|
332
338
|
const renderer = await ssrRenderer(devServer);
|
|
333
|
-
rendered = await renderer.renderDmsPage(page, serverFetch);
|
|
339
|
+
rendered = await renderer.renderDmsPage(page, serverFetch, requestCookies);
|
|
334
340
|
} catch (error) {
|
|
335
341
|
console.error(
|
|
336
342
|
"DMS SSR render failed; falling back to client rendering",
|
|
@@ -348,7 +354,9 @@ export async function renderHtml(page, requestUrl, serverFetch) {
|
|
|
348
354
|
const preloads = await documentStyleTags(devServer, page, template);
|
|
349
355
|
const html = template
|
|
350
356
|
.replace("<title>Antelope DMS</title>", rendered.head.headTags)
|
|
351
|
-
.replace(
|
|
357
|
+
.replace(/<html([^>]*)>/, (_, attributes) =>
|
|
358
|
+
htmlTag(attributes, rendered.head.htmlAttrs),
|
|
359
|
+
)
|
|
352
360
|
.replace("<body", `<body ${rendered.head.bodyAttrs}`)
|
|
353
361
|
.replace("</head>", `${preloads}</head>`)
|
|
354
362
|
.replace(
|
|
@@ -359,6 +367,11 @@ export async function renderHtml(page, requestUrl, serverFetch) {
|
|
|
359
367
|
return { html, status: rendered.error?.statusCode ?? 200 };
|
|
360
368
|
}
|
|
361
369
|
|
|
370
|
+
function renderRequestHtml(page, request) {
|
|
371
|
+
const fetch = serverComponentFetch(request);
|
|
372
|
+
return renderHtml(page, request.url, fetch, request.headers.cookie);
|
|
373
|
+
}
|
|
374
|
+
|
|
362
375
|
async function writeBackendError(error, request, response) {
|
|
363
376
|
const status = Number.isInteger(error?.status) ? error.status : 502;
|
|
364
377
|
const pathname = new URL(request.url, "http://frontend.local").pathname;
|
|
@@ -392,11 +405,7 @@ async function writeBackendError(error, request, response) {
|
|
|
392
405
|
JSON.stringify(page),
|
|
393
406
|
);
|
|
394
407
|
}
|
|
395
|
-
const { html } = await
|
|
396
|
-
page,
|
|
397
|
-
request.url,
|
|
398
|
-
serverComponentFetch(request),
|
|
399
|
-
);
|
|
408
|
+
const { html } = await renderRequestHtml(page, request);
|
|
400
409
|
return writeContent(request, response, status, htmlHeaders(), html);
|
|
401
410
|
}
|
|
402
411
|
const payload =
|
|
@@ -449,11 +458,7 @@ export async function handleRequest(request, response) {
|
|
|
449
458
|
JSON.stringify(page),
|
|
450
459
|
);
|
|
451
460
|
}
|
|
452
|
-
const rendered = await
|
|
453
|
-
page,
|
|
454
|
-
request.url,
|
|
455
|
-
serverComponentFetch(request),
|
|
456
|
-
);
|
|
461
|
+
const rendered = await renderRequestHtml(page, request);
|
|
457
462
|
if (rendered.redirect) {
|
|
458
463
|
redirectFrontendVisit(request, response, rendered.redirect);
|
|
459
464
|
return;
|
|
@@ -5,9 +5,11 @@ import { renderToString } from "@vue/server-renderer";
|
|
|
5
5
|
import { createSSRApp, h } from "vue";
|
|
6
6
|
import {
|
|
7
7
|
type DmsServerFetch,
|
|
8
|
+
SSR_ASYNC_COMPONENTS_ID,
|
|
8
9
|
configureDmsApp,
|
|
9
10
|
resolveDmsInertiaPage,
|
|
10
11
|
serializeDmsAsyncData,
|
|
12
|
+
trackDmsAsyncComponents,
|
|
11
13
|
} from "./app-runtime";
|
|
12
14
|
import {
|
|
13
15
|
type DmsErrorData,
|
|
@@ -60,6 +62,11 @@ function asyncDataScript(runtime: DmsFrontendRuntime): string {
|
|
|
60
62
|
return `<script id="${SSR_ASYNC_DATA_ID}" type="application/json">${value}</script>`;
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
function asyncComponentsScript(names: Set<string>): string {
|
|
66
|
+
const value = JSON.stringify([...names]).replaceAll("<", "\\u003c");
|
|
67
|
+
return `<script id="${SSR_ASYNC_COMPONENTS_ID}" type="application/json">${value}</script>`;
|
|
68
|
+
}
|
|
69
|
+
|
|
63
70
|
await setupFrontendModules(frontendModules);
|
|
64
71
|
|
|
65
72
|
export function isDmsFrontendPage(path: string): boolean {
|
|
@@ -69,11 +76,13 @@ export function isDmsFrontendPage(path: string): boolean {
|
|
|
69
76
|
export async function renderDmsPage(
|
|
70
77
|
page: DmsSsrPage,
|
|
71
78
|
serverFetch?: DmsServerFetch,
|
|
79
|
+
requestCookies?: string,
|
|
72
80
|
): Promise<DmsSsrResult> {
|
|
73
81
|
const runtime = createDmsFrontendRuntime(
|
|
74
82
|
serverFetch as typeof import("ofetch").ofetch,
|
|
75
83
|
{},
|
|
76
84
|
true,
|
|
85
|
+
requestCookies,
|
|
77
86
|
);
|
|
78
87
|
return runtimeStorage.run(runtime, () =>
|
|
79
88
|
renderDmsPageWithRuntime(page, runtime, serverFetch),
|
|
@@ -88,12 +97,14 @@ async function renderDmsPageWithRuntime(
|
|
|
88
97
|
await preloadDmsPage(page.props);
|
|
89
98
|
const head = createHead();
|
|
90
99
|
const ssrContext: DmsSsrContext = {};
|
|
100
|
+
let asyncComponents = new Set<string>();
|
|
91
101
|
const inertiaResult = await createInertiaApp({
|
|
92
102
|
page,
|
|
93
103
|
render: (app) => renderToString(app, ssrContext),
|
|
94
104
|
resolve: resolveDmsInertiaPage,
|
|
95
105
|
async setup({ App, props, plugin }) {
|
|
96
106
|
const app = createSSRApp({ render: () => h(App, props) });
|
|
107
|
+
asyncComponents = trackDmsAsyncComponents(app);
|
|
97
108
|
const configured = await configureDmsApp({
|
|
98
109
|
app,
|
|
99
110
|
head,
|
|
@@ -130,12 +141,13 @@ async function renderDmsPageWithRuntime(
|
|
|
130
141
|
return renderDmsPage(
|
|
131
142
|
{ ...page, props: { ...page.props, error } },
|
|
132
143
|
serverFetch,
|
|
144
|
+
runtime.requestCookies,
|
|
133
145
|
);
|
|
134
146
|
}
|
|
135
147
|
const renderedHead = await renderSSRHead(head);
|
|
136
148
|
return {
|
|
137
149
|
error: page.props.error,
|
|
138
|
-
body: `${inertiaResult.body}${asyncDataScript(runtime)}`,
|
|
150
|
+
body: `${inertiaResult.body}${asyncDataScript(runtime)}${asyncComponentsScript(asyncComponents)}`,
|
|
139
151
|
head: {
|
|
140
152
|
bodyAttrs: renderedHead.bodyAttrs,
|
|
141
153
|
headTags: `${inertiaResult.head.join("")}${renderedHead.headTags}`,
|
|
@@ -184,7 +184,7 @@ export default defineConfig({
|
|
|
184
184
|
paintBeforeHydrate(),
|
|
185
185
|
],
|
|
186
186
|
resolve: {
|
|
187
|
-
dedupe: ["vue", "reka-ui", "@nuxt/ui"],
|
|
187
|
+
dedupe: ["vue", "reka-ui", "@nuxt/ui", "@vueuse/core"],
|
|
188
188
|
alias: {
|
|
189
189
|
"#dms/frontend-module": resolve(__dirname, "frontend-module.ts"),
|
|
190
190
|
"#build/nuxt-icon-client-bundle": "virtual:nuxt-ui-icons",
|