@antelopejs/dms-frontend 0.6.1 → 0.6.3
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 +55 -939
- package/dist/config.js +1 -0
- package/package.json +20 -8
- package/skills/dms-frontend/SKILL.md +50 -0
- package/templates/vue/frontend-module.ts +20 -2
- package/templates/vue/main.ts +36 -23
- package/templates/vue/server/built-files.mjs +48 -0
- package/templates/vue/server/content.mjs +46 -6
- package/templates/vue/server/email-bundle-watch.mjs +24 -3
- package/templates/vue/server/email.mjs +58 -15
- package/templates/vue/server/proxy-compression.mjs +101 -0
- package/templates/vue/server/proxy-headers.mjs +29 -2
- package/templates/vue/server/static-files.mjs +109 -0
- package/templates/vue/server.mjs +26 -50
- package/templates/vue/ui-request-state.mjs +53 -0
- package/templates/vue/vite.config.ts +11 -2
|
@@ -11,8 +11,28 @@ const HOP_BY_HOP_HEADERS = [
|
|
|
11
11
|
"upgrade",
|
|
12
12
|
];
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/** `Accept-Encoding` added to the backend's own `Vary`, once. */
|
|
15
|
+
function varyWithEncoding(vary) {
|
|
16
|
+
const names = (vary ?? "")
|
|
17
|
+
.split(",")
|
|
18
|
+
.map((name) => name.trim())
|
|
19
|
+
.filter(Boolean);
|
|
20
|
+
if (names.some((name) => name === "*" || /^accept-encoding$/i.test(name)))
|
|
21
|
+
return names.join(", ");
|
|
22
|
+
return [...names, "Accept-Encoding"].join(", ");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Writes the backend's status and end-to-end headers on `response`. With an
|
|
27
|
+
* `encoding`, the proxy compresses the body itself: its length is no longer
|
|
28
|
+
* known, and a strong validator no longer names these bytes.
|
|
29
|
+
*/
|
|
30
|
+
export function writeProxyHeaders(
|
|
31
|
+
response,
|
|
32
|
+
upstream,
|
|
33
|
+
encoding,
|
|
34
|
+
varies = false,
|
|
35
|
+
) {
|
|
16
36
|
const headers = Object.fromEntries(upstream.headers);
|
|
17
37
|
const named = (headers.connection ?? "")
|
|
18
38
|
.split(",")
|
|
@@ -21,5 +41,12 @@ export function writeProxyHeaders(response, upstream) {
|
|
|
21
41
|
delete headers["set-cookie"];
|
|
22
42
|
const cookies = upstream.headers.getSetCookie?.() ?? [];
|
|
23
43
|
if (cookies.length) headers["set-cookie"] = cookies;
|
|
44
|
+
if (encoding || varies) headers.vary = varyWithEncoding(headers.vary);
|
|
45
|
+
if (encoding) {
|
|
46
|
+
delete headers["content-length"];
|
|
47
|
+
headers["content-encoding"] = encoding;
|
|
48
|
+
if (headers.etag && !headers.etag.startsWith("W/"))
|
|
49
|
+
headers.etag = `W/${headers.etag}`;
|
|
50
|
+
}
|
|
24
51
|
response.writeHead(upstream.status, headers);
|
|
25
52
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { createReadStream, existsSync, statSync } from "node:fs";
|
|
2
|
+
import { extname } from "node:path";
|
|
3
|
+
|
|
4
|
+
// What a build or a module's `public/` directory may hold. A type missing
|
|
5
|
+
// here went out as `application/octet-stream`: fonts among them, which a
|
|
6
|
+
// browser then reports as a mismatch against the `font/woff2` it preloaded.
|
|
7
|
+
const MIME_TYPES = {
|
|
8
|
+
".avif": "image/avif",
|
|
9
|
+
".css": "text/css",
|
|
10
|
+
".gif": "image/gif",
|
|
11
|
+
".html": "text/html; charset=utf-8",
|
|
12
|
+
".ico": "image/x-icon",
|
|
13
|
+
".jpeg": "image/jpeg",
|
|
14
|
+
".jpg": "image/jpeg",
|
|
15
|
+
".js": "text/javascript",
|
|
16
|
+
".json": "application/json",
|
|
17
|
+
".map": "application/json",
|
|
18
|
+
".mjs": "text/javascript",
|
|
19
|
+
".mp4": "video/mp4",
|
|
20
|
+
".otf": "font/otf",
|
|
21
|
+
".pdf": "application/pdf",
|
|
22
|
+
".png": "image/png",
|
|
23
|
+
".svg": "image/svg+xml",
|
|
24
|
+
".ttf": "font/ttf",
|
|
25
|
+
".txt": "text/plain; charset=utf-8",
|
|
26
|
+
".wasm": "application/wasm",
|
|
27
|
+
".webm": "video/webm",
|
|
28
|
+
".webmanifest": "application/manifest+json",
|
|
29
|
+
".webp": "image/webp",
|
|
30
|
+
".woff": "font/woff",
|
|
31
|
+
".woff2": "font/woff2",
|
|
32
|
+
".xml": "application/xml",
|
|
33
|
+
};
|
|
34
|
+
const DEFAULT_MIME_TYPE = "application/octet-stream";
|
|
35
|
+
// Vite names everything under `assets/` after its content: a new build
|
|
36
|
+
// writes new names, so a client never needs to ask again.
|
|
37
|
+
const IMMUTABLE_CACHE = "public, max-age=31536000, immutable";
|
|
38
|
+
// Any other file keeps its name across builds (a module's `public/` file,
|
|
39
|
+
// a favicon): the client asks again on each use, and is answered 304 while
|
|
40
|
+
// it has not changed.
|
|
41
|
+
const REVALIDATED_CACHE = "no-cache";
|
|
42
|
+
|
|
43
|
+
const isFile = (path) => existsSync(path) && statSync(path).isFile();
|
|
44
|
+
|
|
45
|
+
/** The precompressed variant of `file` the client accepts, if the build wrote one. */
|
|
46
|
+
function acceptedVariant(file, request) {
|
|
47
|
+
const accepted = request.headers["accept-encoding"] ?? "";
|
|
48
|
+
if (accepted.includes("br") && isFile(`${file}.br`))
|
|
49
|
+
return { file: `${file}.br`, encoding: "br" };
|
|
50
|
+
if (accepted.includes("gzip") && isFile(`${file}.gz`))
|
|
51
|
+
return { file: `${file}.gz`, encoding: "gzip" };
|
|
52
|
+
return { file, encoding: undefined };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Weak, as the bytes of one representation are not compared: the size and
|
|
57
|
+
* modification time of the file sent, so each encoding has its own.
|
|
58
|
+
*/
|
|
59
|
+
function entityTag(stats) {
|
|
60
|
+
return `W/"${stats.size.toString(16)}-${Math.floor(stats.mtimeMs).toString(16)}"`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const opaqueTag = (tag) => tag.trim().replace(/^W\//, "");
|
|
64
|
+
|
|
65
|
+
/** RFC 9110 §13.1.2 and §13.1.3: `If-None-Match` first, the date otherwise. */
|
|
66
|
+
function isNotModified(request, tag, stats) {
|
|
67
|
+
const noneMatch = request.headers["if-none-match"];
|
|
68
|
+
if (noneMatch !== undefined) {
|
|
69
|
+
if (noneMatch.trim() === "*") return true;
|
|
70
|
+
const wanted = opaqueTag(tag);
|
|
71
|
+
return noneMatch
|
|
72
|
+
.split(",")
|
|
73
|
+
.some((candidate) => opaqueTag(candidate) === wanted);
|
|
74
|
+
}
|
|
75
|
+
const since = Date.parse(request.headers["if-modified-since"] ?? "");
|
|
76
|
+
// The header has a one-second resolution.
|
|
77
|
+
return (
|
|
78
|
+
!Number.isNaN(since) && Math.floor(stats.mtimeMs / 1000) * 1000 <= since
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Send `file`, a file the caller already resolved and checked (see
|
|
84
|
+
* `publicBuildFile`), as the variant the client accepts, or answer 304 when the
|
|
85
|
+
* client's copy is current. `immutable` for a file named after its content.
|
|
86
|
+
*/
|
|
87
|
+
export function serveStaticFile(file, request, response, { immutable }) {
|
|
88
|
+
const selected = acceptedVariant(file, request);
|
|
89
|
+
const stats = statSync(selected.file);
|
|
90
|
+
const tag = entityTag(stats);
|
|
91
|
+
const headers = {
|
|
92
|
+
"cache-control": immutable ? IMMUTABLE_CACHE : REVALIDATED_CACHE,
|
|
93
|
+
etag: tag,
|
|
94
|
+
"last-modified": stats.mtime.toUTCString(),
|
|
95
|
+
vary: "Accept-Encoding",
|
|
96
|
+
};
|
|
97
|
+
if (isNotModified(request, tag, stats)) {
|
|
98
|
+
response.writeHead(304, headers);
|
|
99
|
+
response.end();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
headers["content-type"] =
|
|
103
|
+
MIME_TYPES[extname(file).toLowerCase()] ?? DEFAULT_MIME_TYPE;
|
|
104
|
+
headers["content-length"] = stats.size;
|
|
105
|
+
if (selected.encoding) headers["content-encoding"] = selected.encoding;
|
|
106
|
+
response.writeHead(200, headers);
|
|
107
|
+
if (request.method === "HEAD") response.end();
|
|
108
|
+
else createReadStream(selected.file).pipe(response);
|
|
109
|
+
}
|
package/templates/vue/server.mjs
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { createServer, maxHeaderSize } from "node:http";
|
|
3
3
|
import { extname, join, resolve, sep } from "node:path";
|
|
4
|
-
import { Readable } from "node:stream";
|
|
5
|
-
import { pipeline } from "node:stream/promises";
|
|
6
4
|
import { fileURLToPath } from "node:url";
|
|
7
5
|
import { ofetch } from "ofetch";
|
|
8
6
|
import {
|
|
@@ -25,6 +23,8 @@ import {
|
|
|
25
23
|
reportReady,
|
|
26
24
|
reportStartError,
|
|
27
25
|
} from "./server/cli-report.mjs";
|
|
26
|
+
import { publicBuildFile } from "./server/built-files.mjs";
|
|
27
|
+
import { serveStaticFile } from "./server/static-files.mjs";
|
|
28
28
|
import { productionHtmlTemplate } from "./server/client-manifest.mjs";
|
|
29
29
|
import { writeContent } from "./server/content.mjs";
|
|
30
30
|
import {
|
|
@@ -49,7 +49,10 @@ import { htmlTag } from "./server/html-tag.mjs";
|
|
|
49
49
|
import { listenHost } from "./server/listen-host.mjs";
|
|
50
50
|
import * as icons from "./server/icons.mjs";
|
|
51
51
|
import * as requestFailure from "./server/request-failure.mjs";
|
|
52
|
-
import {
|
|
52
|
+
import {
|
|
53
|
+
PLAIN_ANSWER_HEADERS,
|
|
54
|
+
relayAnswer,
|
|
55
|
+
} from "./server/proxy-compression.mjs";
|
|
53
56
|
import { backendUrl, refuseForeignTarget } from "./server/request-target.mjs";
|
|
54
57
|
import {
|
|
55
58
|
createInertiaPage,
|
|
@@ -65,13 +68,9 @@ export * from "./server/inertia.mjs";
|
|
|
65
68
|
const INERTIA_HEADER = "x-inertia";
|
|
66
69
|
const JSON_TYPE = "application/json";
|
|
67
70
|
const PROJECT_ROOT = fileURLToPath(new URL(".", import.meta.url));
|
|
68
|
-
const MIME_TYPES = {
|
|
69
|
-
".css": "text/css",
|
|
70
|
-
".js": "text/javascript",
|
|
71
|
-
".svg": "image/svg+xml",
|
|
72
|
-
};
|
|
73
71
|
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
|
74
72
|
const LOCAL_ROUTES = [[icons.isIconRequest, icons.handleIcons]];
|
|
73
|
+
const CLIENT_ROOT = join(PROJECT_ROOT, "dist/client");
|
|
75
74
|
const SOURCE_TEMPLATE_PATH = join(PROJECT_ROOT, "index.html");
|
|
76
75
|
const BUILT_SSR_RENDERER_PATH = join(PROJECT_ROOT, "dist/ssr/ssr-renderer.js");
|
|
77
76
|
// The SSR bundle keeps vue-i18n external, so Node loads its esm-bundler build,
|
|
@@ -215,48 +214,24 @@ async function proxy(request, response, backendProxy) {
|
|
|
215
214
|
const upstream = await fetch(backendUrl(request.url), {
|
|
216
215
|
signal: abandoned.signal,
|
|
217
216
|
method: request.method,
|
|
218
|
-
headers: backendHeaders(request, access),
|
|
217
|
+
headers: { ...backendHeaders(request, access), ...PLAIN_ANSWER_HEADERS },
|
|
219
218
|
body: ["GET", "HEAD"].includes(request.method) ? undefined : request,
|
|
220
219
|
duplex: "half",
|
|
221
220
|
});
|
|
222
|
-
|
|
223
|
-
if (!upstream.body || request.method === "HEAD") response.end();
|
|
224
|
-
else await pipeline(Readable.fromWeb(upstream.body), response);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function acceptedAsset(pathname, request) {
|
|
228
|
-
const file = resolve(PROJECT_ROOT, "dist/client", `.${pathname}`);
|
|
229
|
-
const accepted = request.headers["accept-encoding"] ?? "";
|
|
230
|
-
if (accepted.includes("br") && existsSync(`${file}.br`))
|
|
231
|
-
return { file: `${file}.br`, encoding: "br", source: file };
|
|
232
|
-
if (accepted.includes("gzip") && existsSync(`${file}.gz`))
|
|
233
|
-
return { file: `${file}.gz`, encoding: "gzip", source: file };
|
|
234
|
-
return { file, encoding: undefined, source: file };
|
|
221
|
+
await relayAnswer(request, response, upstream, { fetched: true });
|
|
235
222
|
}
|
|
236
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Send the built file `pathname` names, or `false` when the build has none
|
|
226
|
+
* there. `publicBuildFile` decides which file that is; `serveStaticFile` how
|
|
227
|
+
* it goes out.
|
|
228
|
+
*/
|
|
237
229
|
function serveAsset(pathname, request, response) {
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
!selected.source.startsWith(assetRoot) ||
|
|
244
|
-
!existsSync(selected.file) ||
|
|
245
|
-
!statSync(selected.file).isFile()
|
|
246
|
-
)
|
|
247
|
-
return false;
|
|
248
|
-
const headers = {
|
|
249
|
-
"cache-control": pathname.startsWith("/assets/")
|
|
250
|
-
? "public, max-age=31536000, immutable"
|
|
251
|
-
: "no-cache",
|
|
252
|
-
"content-type":
|
|
253
|
-
MIME_TYPES[extname(selected.source)] ?? "application/octet-stream",
|
|
254
|
-
"content-length": statSync(selected.file).size,
|
|
255
|
-
vary: "Accept-Encoding",
|
|
256
|
-
};
|
|
257
|
-
if (selected.encoding) headers["content-encoding"] = selected.encoding;
|
|
258
|
-
response.writeHead(200, headers);
|
|
259
|
-
createReadStream(selected.file).pipe(response);
|
|
230
|
+
const file = publicBuildFile(CLIENT_ROOT, pathname);
|
|
231
|
+
if (!file) return false;
|
|
232
|
+
serveStaticFile(file, request, response, {
|
|
233
|
+
immutable: pathname.startsWith("/assets/"),
|
|
234
|
+
});
|
|
260
235
|
return true;
|
|
261
236
|
}
|
|
262
237
|
|
|
@@ -391,8 +366,8 @@ async function answeredByVite(devServer, request, response) {
|
|
|
391
366
|
|
|
392
367
|
/**
|
|
393
368
|
* Whether the renderer has a file at `pathname`: a development source Vite
|
|
394
|
-
* serves (under the workspace or its `public/`), or a built asset
|
|
395
|
-
* path with an extension names one.
|
|
369
|
+
* serves (under the workspace or its `public/`), or a built asset the
|
|
370
|
+
* production server serves. Only a path with an extension names one.
|
|
396
371
|
*/
|
|
397
372
|
function hasRendererFile(devServer, requested) {
|
|
398
373
|
let pathname;
|
|
@@ -403,9 +378,10 @@ function hasRendererFile(devServer, requested) {
|
|
|
403
378
|
return false;
|
|
404
379
|
}
|
|
405
380
|
if (extname(pathname) === "") return false;
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
381
|
+
if (!devServer) return publicBuildFile(CLIENT_ROOT, requested) !== undefined;
|
|
382
|
+
const roots = [devServer.config.root, devServer.config.publicDir].filter(
|
|
383
|
+
Boolean,
|
|
384
|
+
);
|
|
409
385
|
return roots.some((root) => {
|
|
410
386
|
const file = resolve(root, `.${pathname}`);
|
|
411
387
|
return (
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Nuxt UI's `useState` outside Nuxt keeps its state in one object for the
|
|
2
|
+
// whole module: on a server, for the whole process. A toast one render adds
|
|
3
|
+
// (`useToast()` keeps its list there) would then show in every later render,
|
|
4
|
+
// whoever asks for the page, until the server restarts. Nuxt UI's stub keeps
|
|
5
|
+
// its state in the DMS's per-application state instead, as Nuxt's own
|
|
6
|
+
// `useState` does, and the build fails if the stub changes form or another
|
|
7
|
+
// Nuxt UI file defines `useState`, rather than letting the leak come back
|
|
8
|
+
// unnoticed with a Nuxt UI release.
|
|
9
|
+
|
|
10
|
+
/** Nuxt UI's stub of Nuxt's composables, which defines `useState`. */
|
|
11
|
+
const STUB = /\/@nuxt\/ui\/dist\/runtime\/vue\/stubs\/base\.js$/;
|
|
12
|
+
/** `useState` as Nuxt UI 4 defines it: a module-wide record of refs. */
|
|
13
|
+
const MODULE_STATE =
|
|
14
|
+
/const state = \{\};\s*export const useState = \(key, init\) => \{\s*if \(state\[key\]\) \{\s*return state\[key\];\s*\}\s*const value = ref\(init\(\)\);\s*state\[key\] = value;\s*return value;\s*\};/;
|
|
15
|
+
/** A definition of `useState`, wherever Nuxt UI moves it. */
|
|
16
|
+
const DEFINES_USE_STATE = /export\s+(?:const|function|let|var)\s+useState\b/;
|
|
17
|
+
const APPLICATION_STATE =
|
|
18
|
+
'import { useDmsState } from "#dms/frontend-module";\n' +
|
|
19
|
+
"export const useState = (key, init) => useDmsState(`nuxt-ui:${key}`, init);";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The rewrite, as a Vite plugin.
|
|
23
|
+
*
|
|
24
|
+
* @param isNuxtUiFile `(id) => boolean`, whether a module is Nuxt UI's
|
|
25
|
+
*/
|
|
26
|
+
export function uiRequestState(isNuxtUiFile) {
|
|
27
|
+
return {
|
|
28
|
+
name: "dms-ui-request-state",
|
|
29
|
+
enforce: "pre",
|
|
30
|
+
transform(code, id) {
|
|
31
|
+
const path = id.replaceAll("\\", "/").split("?")[0];
|
|
32
|
+
if (!isNuxtUiFile(id)) return null;
|
|
33
|
+
if (!STUB.test(path)) {
|
|
34
|
+
if (DEFINES_USE_STATE.test(code))
|
|
35
|
+
this.error(
|
|
36
|
+
`${id} defines useState outside the stub the DMS rewrites: a ` +
|
|
37
|
+
"server render could show another request's state. Update " +
|
|
38
|
+
"ui-request-state.mjs for this Nuxt UI release.",
|
|
39
|
+
);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if (!MODULE_STATE.test(code)) {
|
|
43
|
+
if (!code.includes("useState")) return null;
|
|
44
|
+
this.error(
|
|
45
|
+
`${id} defines useState in a form the DMS does not rewrite: a ` +
|
|
46
|
+
"server render could show another request's state. Update " +
|
|
47
|
+
"ui-request-state.mjs for this Nuxt UI release.",
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return { code: code.replace(MODULE_STATE, APPLICATION_STATE), map: null };
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "vite";
|
|
12
12
|
import { rejectServerSourcesInClient } from "./client-server-guard.mjs";
|
|
13
13
|
import { uiInertiaPage } from "./ui-inertia-page.mjs";
|
|
14
|
+
import { uiRequestState } from "./ui-request-state.mjs";
|
|
14
15
|
import { autoImportDirectories } from "./frontend-build-loader";
|
|
15
16
|
import { frontendBuilds } from "./frontend-builds.generated";
|
|
16
17
|
import { orderHeadForFirstPaint } from "./head-order.mjs";
|
|
@@ -64,6 +65,12 @@ const importDirectories = autoImportDirectories(
|
|
|
64
65
|
frontendBuilds,
|
|
65
66
|
(message) => logger.warn(message),
|
|
66
67
|
);
|
|
68
|
+
/**
|
|
69
|
+
* Dependencies pre-bundled by name. A module's own dependencies need no entry:
|
|
70
|
+
* the startup crawl below (`optimizerEntries`) finds them in its sources. A
|
|
71
|
+
* name no module installs any more only earns a "Failed to resolve
|
|
72
|
+
* dependency" warning at every start.
|
|
73
|
+
*/
|
|
67
74
|
const optimizedDependencies = [
|
|
68
75
|
"vue",
|
|
69
76
|
"@inertiajs/vue3",
|
|
@@ -73,7 +80,6 @@ const optimizedDependencies = [
|
|
|
73
80
|
"@vueuse/core",
|
|
74
81
|
"reka-ui",
|
|
75
82
|
"@internationalized/date",
|
|
76
|
-
"vuedraggable",
|
|
77
83
|
"vue3-apexcharts",
|
|
78
84
|
"apexcharts",
|
|
79
85
|
"tailwind-variants",
|
|
@@ -130,7 +136,9 @@ const uiInertiaLinkImport = resolve(
|
|
|
130
136
|
* Inertia keeps a single page for the whole process, written by each server
|
|
131
137
|
* render as it starts: a link rendered after an await could take another
|
|
132
138
|
* request's URL to set its active state. Their `usePage()` is the DMS one,
|
|
133
|
-
* which gives the page of the request being rendered.
|
|
139
|
+
* which gives the page of the request being rendered. Likewise, their
|
|
140
|
+
* `useState()` (the toasts among others) is the DMS's per-application state,
|
|
141
|
+
* not one record for the whole process.
|
|
134
142
|
*/
|
|
135
143
|
const isNuxtUiFile = (id: string) =>
|
|
136
144
|
normalizePath(id).includes("/node_modules/@nuxt/ui/");
|
|
@@ -149,6 +157,7 @@ export default defineConfig({
|
|
|
149
157
|
},
|
|
150
158
|
},
|
|
151
159
|
uiInertiaPage(isNuxtUiFile),
|
|
160
|
+
uiRequestState(isNuxtUiFile),
|
|
152
161
|
vue(),
|
|
153
162
|
ui({
|
|
154
163
|
router: "inertia",
|