@antelopejs/dms-frontend 0.2.1 → 0.2.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 +5 -3
- package/dist/config.js +1 -0
- package/dist/fs-sync.js +5 -1
- package/dist/materialize.js +2 -2
- package/dist/update-check.js +19 -7
- package/package.json +1 -1
- package/templates/vue/head-order.mjs +39 -0
- package/templates/vue/main.ts +37 -0
- package/templates/vue/server/dev-styles.mjs +106 -0
- package/templates/vue/server/homepage.mjs +32 -0
- package/templates/vue/server/inertia.mjs +21 -0
- package/templates/vue/server.mjs +11 -35
- package/templates/vue/vite.config.ts +55 -5
- package/templates/vue/vite.email.config.ts +5 -2
package/README.md
CHANGED
|
@@ -94,9 +94,11 @@ URL, either through `-b` or through the enclosing antelope project's
|
|
|
94
94
|
`.antelope/dev.json`, and say so on exit 1 when they have neither;
|
|
95
95
|
`verify-source` needs `--layer` instead.
|
|
96
96
|
|
|
97
|
-
The CLI checks npm for a newer release at most once a day
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
The CLI checks npm for a newer release at most once a day and prints a one-line
|
|
98
|
+
notice on stderr. A lookup that comes back empty — offline, throttled, or simply
|
|
99
|
+
raced by a command that blocked the event loop past the deadline — is retried
|
|
100
|
+
after an hour instead of counting as the day's attempt. The throttle stamp lives
|
|
101
|
+
at `~/.antelopejs/dms-frontend/update-check.json`. Set
|
|
100
102
|
`NO_UPDATE_NOTIFIER=1`, pass `--no-update-check`, or run under `CI` to turn the
|
|
101
103
|
check off.
|
|
102
104
|
|
package/dist/config.js
CHANGED
|
@@ -39,6 +39,7 @@ exports.TEMPLATE_FILES = [
|
|
|
39
39
|
["frontend-module.ts", "frontend-module.ts"],
|
|
40
40
|
["globals.d.ts", "globals.d.ts"],
|
|
41
41
|
["compress-assets.mjs", "compress-assets.mjs"],
|
|
42
|
+
["head-order.mjs", "head-order.mjs"],
|
|
42
43
|
["email-renderer.ts", "email-renderer.ts"],
|
|
43
44
|
["email-runtime.ts", "email-runtime.ts"],
|
|
44
45
|
["email-locales.ts", "email-locales.ts"],
|
package/dist/fs-sync.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.stripExtendedLengthPrefix = stripExtendedLengthPrefix;
|
|
7
|
+
exports.toPosixPath = toPosixPath;
|
|
7
8
|
exports.isBlocklistedCopyPath = isBlocklistedCopyPath;
|
|
8
9
|
exports.sanitizedPackageContent = sanitizedPackageContent;
|
|
9
10
|
exports.filesIdentical = filesIdentical;
|
|
@@ -18,11 +19,14 @@ const config_1 = require("./config");
|
|
|
18
19
|
function stripExtendedLengthPrefix(p) {
|
|
19
20
|
return p.replace(/^\\\\\?\\/, "");
|
|
20
21
|
}
|
|
22
|
+
function toPosixPath(path, separator = node_path_1.sep) {
|
|
23
|
+
return separator === "/" ? path : path.split(separator).join("/");
|
|
24
|
+
}
|
|
21
25
|
function isBlocklistedCopyPath(src, srcPath) {
|
|
22
26
|
const rel = (0, node_path_1.relative)((0, node_path_1.resolve)(stripExtendedLengthPrefix(src)), (0, node_path_1.resolve)(stripExtendedLengthPrefix(srcPath)));
|
|
23
27
|
if (!rel || rel.startsWith(".."))
|
|
24
28
|
return false;
|
|
25
|
-
return config_1.layerCopyIgnore.ignores(rel
|
|
29
|
+
return config_1.layerCopyIgnore.ignores(toPosixPath(rel));
|
|
26
30
|
}
|
|
27
31
|
function sanitizedPackageContent(raw) {
|
|
28
32
|
const pkg = JSON.parse(raw);
|
package/dist/materialize.js
CHANGED
|
@@ -198,7 +198,7 @@ function discoverAssets(registry, directory, extensions) {
|
|
|
198
198
|
.sort()
|
|
199
199
|
.map((relativePath) => ({
|
|
200
200
|
moduleId: module.id,
|
|
201
|
-
relativePath: `${assetRoot.relativePrefix}${(0, node_path_1.join)(directory, relativePath)
|
|
201
|
+
relativePath: `${assetRoot.relativePrefix}${(0, fs_sync_1.toPosixPath)((0, node_path_1.join)(directory, relativePath))}`,
|
|
202
202
|
}));
|
|
203
203
|
});
|
|
204
204
|
});
|
|
@@ -259,7 +259,7 @@ function writeShortcutExports(workspaceDir, registry) {
|
|
|
259
259
|
}
|
|
260
260
|
function writeDmsMainCss(workspaceDir, layers) {
|
|
261
261
|
const sources = layers
|
|
262
|
-
.map((layer) => `@source "${(0, layers_1.getLayerWorkspacePath)(workspaceDir, layer)}/${config_1.TAILWIND_SOURCE_GLOB}";`)
|
|
262
|
+
.map((layer) => `@source "${(0, fs_sync_1.toPosixPath)((0, layers_1.getLayerWorkspacePath)(workspaceDir, layer))}/${config_1.TAILWIND_SOURCE_GLOB}";`)
|
|
263
263
|
.join("\n");
|
|
264
264
|
const content = `@import "tailwindcss";
|
|
265
265
|
@import "@nuxt/ui";
|
package/dist/update-check.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.NO_UPDATE_CHECK_FLAG = exports.UPDATE_CHECK_TIMEOUT_MS = exports.UPDATE_CHECK_INTERVAL_MS = exports.UPDATE_CHECK_PACKAGE = void 0;
|
|
6
|
+
exports.NO_UPDATE_CHECK_FLAG = exports.UPDATE_CHECK_TIMEOUT_MS = exports.UPDATE_CHECK_RETRY_INTERVAL_MS = exports.UPDATE_CHECK_INTERVAL_MS = exports.UPDATE_CHECK_PACKAGE = void 0;
|
|
7
7
|
exports.stripUpdateCheckFlag = stripUpdateCheckFlag;
|
|
8
8
|
exports.isUpdateCheckEnabled = isUpdateCheckEnabled;
|
|
9
9
|
exports.updateCheckCacheFile = updateCheckCacheFile;
|
|
@@ -18,6 +18,7 @@ const config_1 = require("./config");
|
|
|
18
18
|
exports.UPDATE_CHECK_PACKAGE = "@antelopejs/dms-frontend";
|
|
19
19
|
const DEFAULT_REGISTRY_URL = "https://registry.npmjs.org/";
|
|
20
20
|
exports.UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
21
|
+
exports.UPDATE_CHECK_RETRY_INTERVAL_MS = 60 * 60 * 1000;
|
|
21
22
|
exports.UPDATE_CHECK_TIMEOUT_MS = 3000;
|
|
22
23
|
exports.NO_UPDATE_CHECK_FLAG = "--no-update-check";
|
|
23
24
|
const HELP_OR_VERSION_FLAGS = ["--help", "-h", "--version", "-v"];
|
|
@@ -73,7 +74,11 @@ function readCache(file) {
|
|
|
73
74
|
if (latestVersion !== undefined && typeof latestVersion !== "string") {
|
|
74
75
|
return undefined;
|
|
75
76
|
}
|
|
76
|
-
|
|
77
|
+
const succeeded = parsed.succeeded;
|
|
78
|
+
if (succeeded !== undefined && typeof succeeded !== "boolean") {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
return { checkedAt: parsed.checkedAt, latestVersion, succeeded };
|
|
77
82
|
}
|
|
78
83
|
catch {
|
|
79
84
|
return undefined;
|
|
@@ -156,13 +161,20 @@ async function checkForUpdate(options) {
|
|
|
156
161
|
return;
|
|
157
162
|
const cached = readCache(cacheFile);
|
|
158
163
|
const checkedAt = now();
|
|
159
|
-
const
|
|
160
|
-
|
|
164
|
+
const lastAttemptAnswered = cached?.succeeded ?? cached?.latestVersion !== undefined;
|
|
165
|
+
const interval = lastAttemptAnswered
|
|
166
|
+
? exports.UPDATE_CHECK_INTERVAL_MS
|
|
167
|
+
: exports.UPDATE_CHECK_RETRY_INTERVAL_MS;
|
|
168
|
+
const withinInterval = cached !== undefined && checkedAt - cached.checkedAt < interval;
|
|
161
169
|
let latestVersion = cached?.latestVersion;
|
|
162
170
|
if (!withinInterval) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
writeCache(cacheFile, {
|
|
171
|
+
const published = await fetchLatestVersion(exports.UPDATE_CHECK_PACKAGE);
|
|
172
|
+
latestVersion = published ?? latestVersion;
|
|
173
|
+
writeCache(cacheFile, {
|
|
174
|
+
checkedAt,
|
|
175
|
+
latestVersion,
|
|
176
|
+
succeeded: published !== undefined,
|
|
177
|
+
});
|
|
166
178
|
}
|
|
167
179
|
if (latestVersion && isNewer(currentVersion, latestVersion)) {
|
|
168
180
|
write(formatNotice(currentVersion, latestVersion));
|
package/package.json
CHANGED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Restoring "paint before hydrate" in the built document.
|
|
2
|
+
//
|
|
3
|
+
// The source index.html marks the entry script `fetchpriority="low"` so the
|
|
4
|
+
// browser spends its bandwidth on the render-blocking stylesheet first. Vite
|
|
5
|
+
// rewrites that script tag when it builds and drops the attribute, and it emits
|
|
6
|
+
// the script *before* the stylesheet — so the production document did the
|
|
7
|
+
// opposite of what the template asked: it discovered a 250 kB hydration bundle
|
|
8
|
+
// first and let it compete with the 27 kB the first paint actually waits on,
|
|
9
|
+
// over the plain HTTP/1.1 the generated server speaks.
|
|
10
|
+
//
|
|
11
|
+
// Kept beside vite.config.ts rather than inside it so the transform can be
|
|
12
|
+
// exercised on its own, the way client-manifest.mjs is.
|
|
13
|
+
|
|
14
|
+
const SCRIPT = /<script[^>]*\stype="module"[^>]*><\/script>/;
|
|
15
|
+
const STYLESHEET = /[ \t]*<link rel="stylesheet"[^>]*>\n?/;
|
|
16
|
+
|
|
17
|
+
function deprioritize(html) {
|
|
18
|
+
return html.replace(SCRIPT, (tag) =>
|
|
19
|
+
tag.includes("fetchpriority")
|
|
20
|
+
? tag
|
|
21
|
+
: tag.replace("<script ", '<script fetchpriority="low" '),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Give the entry script back its low fetch priority and move the stylesheet
|
|
27
|
+
* ahead of it. A document that already has them in that order is returned
|
|
28
|
+
* unchanged, so the transform is safe to run twice.
|
|
29
|
+
*/
|
|
30
|
+
export function orderHeadForFirstPaint(html) {
|
|
31
|
+
const prioritized = deprioritize(html);
|
|
32
|
+
const stylesheet = prioritized.match(STYLESHEET);
|
|
33
|
+
const script = prioritized.match(SCRIPT);
|
|
34
|
+
if (!stylesheet || !script || stylesheet.index < script.index)
|
|
35
|
+
return prioritized;
|
|
36
|
+
return prioritized
|
|
37
|
+
.replace(STYLESHEET, "")
|
|
38
|
+
.replace(SCRIPT, (tag) => `${stylesheet[0].trim()}\n ${tag}`);
|
|
39
|
+
}
|
package/templates/vue/main.ts
CHANGED
|
@@ -6,10 +6,47 @@ import { configureDmsApp, resolveDmsInertiaPage } from "./app-runtime";
|
|
|
6
6
|
import { setupFrontendModules } from "./frontend-module";
|
|
7
7
|
import { frontendModules } from "./frontend-modules.generated";
|
|
8
8
|
|
|
9
|
+
const DEV_STYLE_ATTRIBUTE = "data-dms-dev-style";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Hand each development stylesheet over to Vite.
|
|
13
|
+
*
|
|
14
|
+
* The development server puts the page's stylesheets in the document so it
|
|
15
|
+
* never paints unstyled (see `server/dev-styles.mjs`). Vite then injects its
|
|
16
|
+
* own copy of each one as a `<style data-vite-dev-id>`, and keeping both would
|
|
17
|
+
* make a later CSS edit look like it removed nothing: Vite rewrites its copy,
|
|
18
|
+
* ours keeps the deleted rules alive.
|
|
19
|
+
*
|
|
20
|
+
* We drop ours the moment Vite's appears, keyed by module id, rather than at a
|
|
21
|
+
* fixed point in the boot. A component reached through a dynamic import — most
|
|
22
|
+
* of them — has not been loaded when the app mounts, so removing its scoped
|
|
23
|
+
* rules then would flash exactly what this whole change exists to prevent.
|
|
24
|
+
*/
|
|
25
|
+
function adoptViteDevStyles(): void {
|
|
26
|
+
const pending = new Map<string, Element>();
|
|
27
|
+
for (const node of document.querySelectorAll(`[${DEV_STYLE_ATTRIBUTE}]`)) {
|
|
28
|
+
pending.set(node.getAttribute(DEV_STYLE_ATTRIBUTE) ?? "", node);
|
|
29
|
+
}
|
|
30
|
+
if (pending.size === 0) return;
|
|
31
|
+
const adopt = (): void => {
|
|
32
|
+
for (const style of document.querySelectorAll("style[data-vite-dev-id]")) {
|
|
33
|
+
const id = style.getAttribute("data-vite-dev-id") ?? "";
|
|
34
|
+
pending.get(id)?.remove();
|
|
35
|
+
pending.delete(id);
|
|
36
|
+
}
|
|
37
|
+
if (pending.size === 0) observer.disconnect();
|
|
38
|
+
};
|
|
39
|
+
const observer = new MutationObserver(adopt);
|
|
40
|
+
observer.observe(document.head, { childList: true });
|
|
41
|
+
adopt();
|
|
42
|
+
}
|
|
43
|
+
|
|
9
44
|
function markDmsReady(): void {
|
|
10
45
|
document.documentElement.dataset.dmsReady = "true";
|
|
11
46
|
}
|
|
12
47
|
|
|
48
|
+
if (import.meta.env.DEV) adoptViteDevStyles();
|
|
49
|
+
|
|
13
50
|
await setupFrontendModules(frontendModules);
|
|
14
51
|
|
|
15
52
|
createInertiaApp({
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Putting the page's stylesheets in the document the development server
|
|
2
|
+
// returns, instead of letting the client entry inject them from JavaScript
|
|
3
|
+
// once it has executed.
|
|
4
|
+
//
|
|
5
|
+
// Vite serves every stylesheet as a JavaScript module in development, so a
|
|
6
|
+
// plain SSR document carries no styles at all and the browser paints raw HTML
|
|
7
|
+
// until `/main.ts` and its module graph have run. Production has no such gap:
|
|
8
|
+
// the built `index.html` already links the entry stylesheet, which is why this
|
|
9
|
+
// module is development-only and lives beside client-manifest.mjs rather than
|
|
10
|
+
// inside server.mjs, whose size the linter caps.
|
|
11
|
+
//
|
|
12
|
+
// Each tag carries the module id Vite will use for its own copy
|
|
13
|
+
// (`data-vite-dev-id`), under our own attribute name. `main.ts` hands the
|
|
14
|
+
// stylesheet over as soon as Vite injects that copy — see `adoptViteDevStyles`.
|
|
15
|
+
// We deliberately do NOT reuse `data-vite-dev-id` itself: Vite's client adopts
|
|
16
|
+
// any pre-existing link with that attribute and then skips `updateStyle` for
|
|
17
|
+
// it, which would leave CSS edits stranded.
|
|
18
|
+
|
|
19
|
+
import { join, sep } from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
import { pageModulePreloads } from "./client-manifest.mjs";
|
|
22
|
+
|
|
23
|
+
const PROJECT_ROOT = fileURLToPath(new URL("../", import.meta.url));
|
|
24
|
+
const STYLE_ATTRIBUTE = "data-dms-dev-style";
|
|
25
|
+
const SFC_STYLE = /[?&]vue&type=style/;
|
|
26
|
+
const STYLE_EXTENSION =
|
|
27
|
+
/\.(?:css|less|pcss|postcss|sass|scss|styl|stylus)(?:$|\?)/;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The workspace's generated entry stylesheet. `main.ts` is its only importer,
|
|
31
|
+
* so it never enters the SSR module graph, yet it carries Tailwind, Nuxt UI and
|
|
32
|
+
* one `@source` per materialized module — nearly every rule a page needs.
|
|
33
|
+
* Naming it is the one enumeration this module accepts, and the comment is the
|
|
34
|
+
* reason why.
|
|
35
|
+
*
|
|
36
|
+
* `ENTRY_STYLESHEET_ID` is matched against the `data-vite-dev-id` Vite puts on
|
|
37
|
+
* its own copy, and Vite normalizes every module id onto POSIX separators — so
|
|
38
|
+
* the native path `join` returns has to be normalized too, or the handover
|
|
39
|
+
* never happens on Windows and both copies stay in the document.
|
|
40
|
+
*/
|
|
41
|
+
const ENTRY_STYLESHEET = "/dms-main.css";
|
|
42
|
+
const ENTRY_STYLESHEET_ID = join(PROJECT_ROOT, "dms-main.css")
|
|
43
|
+
.split(sep)
|
|
44
|
+
.join("/");
|
|
45
|
+
|
|
46
|
+
function ssrModuleGraph(devServer) {
|
|
47
|
+
return devServer.environments?.ssr?.moduleGraph ?? devServer.moduleGraph;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function directUrl(url) {
|
|
51
|
+
return `${url}${url.includes("?") ? "&" : "?"}direct`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function styleModules(devServer) {
|
|
55
|
+
const graph = ssrModuleGraph(devServer);
|
|
56
|
+
if (!graph) return [];
|
|
57
|
+
return [...graph.urlToModuleMap.entries()]
|
|
58
|
+
.filter(([url]) => SFC_STYLE.test(url) || STYLE_EXTENSION.test(url))
|
|
59
|
+
.map(([url, module]) => ({ url, id: module?.id ?? url }));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function linkTag({ url, id }) {
|
|
63
|
+
return `<link rel="stylesheet" ${STYLE_ATTRIBUTE}="${id}" href="${directUrl(url)}">`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A single-file component's `<style>` block is served with a JavaScript content
|
|
68
|
+
* type even when requested directly, so a browser would refuse it as a
|
|
69
|
+
* stylesheet. They are small; inline them instead. A block that could close its
|
|
70
|
+
* own tag is dropped rather than escaped — CSS has no escape that is valid in
|
|
71
|
+
* every position, and a scoped component rule is not worth the risk.
|
|
72
|
+
*/
|
|
73
|
+
async function inlineTag(devServer, { url, id }) {
|
|
74
|
+
const result = await devServer
|
|
75
|
+
.transformRequest(directUrl(url))
|
|
76
|
+
.catch(() => undefined);
|
|
77
|
+
const css = result?.code;
|
|
78
|
+
if (!css || /<\/style/i.test(css)) return "";
|
|
79
|
+
return `<style ${STYLE_ATTRIBUTE}="${id}">${css}</style>`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Style tags for everything the development render touched, entry sheet first. */
|
|
83
|
+
export async function developmentStyleTags(devServer) {
|
|
84
|
+
const modules = styleModules(devServer);
|
|
85
|
+
const links = [
|
|
86
|
+
{ url: ENTRY_STYLESHEET, id: ENTRY_STYLESHEET_ID },
|
|
87
|
+
...modules.filter(({ url }) => !SFC_STYLE.test(url)),
|
|
88
|
+
].map(linkTag);
|
|
89
|
+
const inlined = await Promise.all(
|
|
90
|
+
modules
|
|
91
|
+
.filter(({ url }) => SFC_STYLE.test(url))
|
|
92
|
+
.map((module) => inlineTag(devServer, module)),
|
|
93
|
+
);
|
|
94
|
+
return [...links, ...inlined].join("");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The style tags a document needs, whichever server is answering it: collected
|
|
99
|
+
* from the live module graph in development, read from the build manifest in
|
|
100
|
+
* production.
|
|
101
|
+
*/
|
|
102
|
+
export async function documentStyleTags(devServer, page, template) {
|
|
103
|
+
return devServer
|
|
104
|
+
? developmentStyleTags(devServer)
|
|
105
|
+
: pageModulePreloads(page, template);
|
|
106
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Working out which page a visit to `/` means.
|
|
2
|
+
//
|
|
3
|
+
// The answer lives in the module registry the workspace was materialized with:
|
|
4
|
+
// a module declares `homepage` somewhere in its options, at whatever depth its
|
|
5
|
+
// own configuration shape puts it, and the highest-priority declaration wins —
|
|
6
|
+
// the registry is already sorted. Read once at boot, because the registry is a
|
|
7
|
+
// build artifact and cannot change while the server is up.
|
|
8
|
+
//
|
|
9
|
+
// Split out of server.mjs, whose size the linter caps.
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
|
|
15
|
+
const PROJECT_ROOT = fileURLToPath(new URL("../", import.meta.url));
|
|
16
|
+
const REGISTRY_PATH = join(PROJECT_ROOT, "generated-frontend-modules.json");
|
|
17
|
+
const REGISTRY = existsSync(REGISTRY_PATH)
|
|
18
|
+
? JSON.parse(readFileSync(REGISTRY_PATH, "utf8"))
|
|
19
|
+
: { modules: [] };
|
|
20
|
+
|
|
21
|
+
function findHomepage(options) {
|
|
22
|
+
if (!options || typeof options !== "object") return undefined;
|
|
23
|
+
if (typeof options.homepage === "string") return options.homepage;
|
|
24
|
+
return Object.values(options)
|
|
25
|
+
.map(findHomepage)
|
|
26
|
+
.find((homepage) => homepage !== undefined);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const HOMEPAGE =
|
|
30
|
+
REGISTRY.modules
|
|
31
|
+
.map((module) => findHomepage(module.options))
|
|
32
|
+
.find(Boolean) ?? "/";
|
|
@@ -7,6 +7,17 @@ const CLIENT_MANIFEST_PATH = fileURLToPath(
|
|
|
7
7
|
);
|
|
8
8
|
let productionAssetVersion;
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Every document and Inertia payload this server writes carries the visitor's
|
|
12
|
+
* own session: the user, the session, and the page tree their permissions
|
|
13
|
+
* allow. `private` is what keeps a shared cache from ever holding it;
|
|
14
|
+
* `no-cache` keeps the browser's own copy from being reused without asking us
|
|
15
|
+
* first, so a revoked session cannot resurface from disk. We stop short of
|
|
16
|
+
* `no-store`, which would additionally forfeit the back-forward cache on every
|
|
17
|
+
* page of a dashboard people navigate constantly.
|
|
18
|
+
*/
|
|
19
|
+
export const PRIVATE_CACHE_CONTROL = "private, no-cache";
|
|
20
|
+
|
|
10
21
|
export function assetVersion() {
|
|
11
22
|
if (process.env.DMS_DEV === "true" || !existsSync(CLIENT_MANIFEST_PATH))
|
|
12
23
|
return "development";
|
|
@@ -32,6 +43,7 @@ export function createInertiaPage(url, props, version = assetVersion()) {
|
|
|
32
43
|
|
|
33
44
|
export function inertiaHeaders(version = assetVersion()) {
|
|
34
45
|
return {
|
|
46
|
+
"cache-control": PRIVATE_CACHE_CONTROL,
|
|
35
47
|
"content-type": "application/json",
|
|
36
48
|
"x-inertia": "true",
|
|
37
49
|
vary: "X-Inertia",
|
|
@@ -39,6 +51,15 @@ export function inertiaHeaders(version = assetVersion()) {
|
|
|
39
51
|
};
|
|
40
52
|
}
|
|
41
53
|
|
|
54
|
+
/** Headers for a rendered document: same cache stance as an Inertia payload. */
|
|
55
|
+
export function htmlHeaders() {
|
|
56
|
+
return {
|
|
57
|
+
"cache-control": PRIVATE_CACHE_CONTROL,
|
|
58
|
+
"content-type": "text/html",
|
|
59
|
+
vary: "X-Inertia",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
42
63
|
export function handleAssetVersionMismatch(request, response) {
|
|
43
64
|
if (
|
|
44
65
|
!request.headers["x-inertia"] ||
|
package/templates/vue/server.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createReadStream, existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
|
-
import { extname, join, resolve } from "node:path";
|
|
3
|
+
import { extname, join, resolve, sep } from "node:path";
|
|
4
4
|
import { Readable } from "node:stream";
|
|
5
5
|
import { pipeline } from "node:stream/promises";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -18,14 +18,14 @@ import {
|
|
|
18
18
|
refreshSession,
|
|
19
19
|
} from "./server/auth/routes.mjs";
|
|
20
20
|
import { readSession } from "./server/auth/session.mjs";
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
productionHtmlTemplate,
|
|
24
|
-
} from "./server/client-manifest.mjs";
|
|
21
|
+
import { productionHtmlTemplate } from "./server/client-manifest.mjs";
|
|
22
|
+
import { documentStyleTags } from "./server/dev-styles.mjs";
|
|
25
23
|
import { handleEmailRender } from "./server/email.mjs";
|
|
24
|
+
import { HOMEPAGE } from "./server/homepage.mjs";
|
|
26
25
|
import { handleTester } from "./server/tester.mjs";
|
|
27
26
|
import {
|
|
28
27
|
createInertiaPage,
|
|
28
|
+
htmlHeaders,
|
|
29
29
|
inertiaAppHtml,
|
|
30
30
|
inertiaHeaders,
|
|
31
31
|
redirectFrontendVisit,
|
|
@@ -36,13 +36,6 @@ export * from "./server/inertia.mjs";
|
|
|
36
36
|
const INERTIA_HEADER = "x-inertia";
|
|
37
37
|
const JSON_TYPE = "application/json";
|
|
38
38
|
const PROJECT_ROOT = fileURLToPath(new URL(".", import.meta.url));
|
|
39
|
-
const MODULE_REGISTRY_PATH = join(
|
|
40
|
-
PROJECT_ROOT,
|
|
41
|
-
"generated-frontend-modules.json",
|
|
42
|
-
);
|
|
43
|
-
const MODULE_REGISTRY = existsSync(MODULE_REGISTRY_PATH)
|
|
44
|
-
? JSON.parse(readFileSync(MODULE_REGISTRY_PATH, "utf8"))
|
|
45
|
-
: { modules: [] };
|
|
46
39
|
const MIME_TYPES = {
|
|
47
40
|
".css": "text/css",
|
|
48
41
|
".js": "text/javascript",
|
|
@@ -74,19 +67,6 @@ let vite;
|
|
|
74
67
|
let vitePromise;
|
|
75
68
|
let frontendHttpServer;
|
|
76
69
|
|
|
77
|
-
function findHomepage(options) {
|
|
78
|
-
if (!options || typeof options !== "object") return undefined;
|
|
79
|
-
if (typeof options.homepage === "string") return options.homepage;
|
|
80
|
-
return Object.values(options)
|
|
81
|
-
.map(findHomepage)
|
|
82
|
-
.find((homepage) => homepage !== undefined);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const HOMEPAGE =
|
|
86
|
-
MODULE_REGISTRY.modules
|
|
87
|
-
.map((module) => findHomepage(module.options))
|
|
88
|
-
.find(Boolean) ?? "/";
|
|
89
|
-
|
|
90
70
|
class BackendResponseError extends Error {
|
|
91
71
|
constructor(status) {
|
|
92
72
|
super(`DMS backend returned ${status}`);
|
|
@@ -274,7 +254,9 @@ function acceptedAsset(pathname, request) {
|
|
|
274
254
|
|
|
275
255
|
function serveAsset(pathname, request, response) {
|
|
276
256
|
const selected = acceptedAsset(pathname, request);
|
|
277
|
-
|
|
257
|
+
// Native separator: `selected.source` comes from `resolve`, so a hardcoded
|
|
258
|
+
// `/` here would make the containment check fail for every asset on Windows.
|
|
259
|
+
const assetRoot = `${resolve(PROJECT_ROOT, "dist/client")}${sep}`;
|
|
278
260
|
if (
|
|
279
261
|
!selected.source.startsWith(assetRoot) ||
|
|
280
262
|
!existsSync(selected.file) ||
|
|
@@ -358,7 +340,7 @@ export async function renderHtml(page, requestUrl, serverFetch) {
|
|
|
358
340
|
}
|
|
359
341
|
if (rendered.redirect)
|
|
360
342
|
return { html: "", status: 200, redirect: rendered.redirect };
|
|
361
|
-
const preloads = devServer
|
|
343
|
+
const preloads = await documentStyleTags(devServer, page, template);
|
|
362
344
|
const html = template
|
|
363
345
|
.replace("<title>Antelope DMS</title>", rendered.head.headTags)
|
|
364
346
|
.replace("<html", `<html ${rendered.head.htmlAttrs}`)
|
|
@@ -410,13 +392,7 @@ async function writeBackendError(error, request, response) {
|
|
|
410
392
|
request.url,
|
|
411
393
|
serverComponentFetch(request),
|
|
412
394
|
);
|
|
413
|
-
return writeContent(
|
|
414
|
-
request,
|
|
415
|
-
response,
|
|
416
|
-
status,
|
|
417
|
-
{ "content-type": "text/html", vary: "X-Inertia" },
|
|
418
|
-
html,
|
|
419
|
-
);
|
|
395
|
+
return writeContent(request, response, status, htmlHeaders(), html);
|
|
420
396
|
}
|
|
421
397
|
const payload =
|
|
422
398
|
error instanceof UpstreamError || error instanceof RequestBodyError
|
|
@@ -481,7 +457,7 @@ export async function handleRequest(request, response) {
|
|
|
481
457
|
request,
|
|
482
458
|
response,
|
|
483
459
|
rendered.status,
|
|
484
|
-
|
|
460
|
+
htmlHeaders(),
|
|
485
461
|
rendered.html,
|
|
486
462
|
);
|
|
487
463
|
}
|
|
@@ -2,7 +2,8 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { basename, resolve } from "node:path";
|
|
3
3
|
import ui from "@nuxt/ui/vite";
|
|
4
4
|
import vue from "@vitejs/plugin-vue";
|
|
5
|
-
import { defineConfig } from "vite";
|
|
5
|
+
import { defineConfig, normalizePath, type Plugin } from "vite";
|
|
6
|
+
import { orderHeadForFirstPaint } from "./head-order.mjs";
|
|
6
7
|
|
|
7
8
|
interface FrontendModuleRegistryEntry {
|
|
8
9
|
id: string;
|
|
@@ -37,6 +38,14 @@ const stableModuleAliases = Object.fromEntries(
|
|
|
37
38
|
return [[`#${moduleName}`, module.root]];
|
|
38
39
|
}),
|
|
39
40
|
);
|
|
41
|
+
/**
|
|
42
|
+
* Globs are POSIX, always.
|
|
43
|
+
*
|
|
44
|
+
* `resolve` returns a native path, so on Windows every pattern below would
|
|
45
|
+
* carry backslashes — which a glob matcher reads as escape characters, not as
|
|
46
|
+
* separators, and which therefore match nothing at all. `normalizePath` is
|
|
47
|
+
* Vite's own answer to this and is the identity function on POSIX.
|
|
48
|
+
*/
|
|
40
49
|
const importDirectories = frontendSourceRoots.flatMap((root) =>
|
|
41
50
|
[
|
|
42
51
|
"app/composables/**/*",
|
|
@@ -44,7 +53,7 @@ const importDirectories = frontendSourceRoots.flatMap((root) =>
|
|
|
44
53
|
"app/utils/**/*",
|
|
45
54
|
"app/build/composables/**/*",
|
|
46
55
|
"app/build/types/**/*",
|
|
47
|
-
].map((glob) => ({ glob: resolve(root, glob), types: true })),
|
|
56
|
+
].map((glob) => ({ glob: normalizePath(resolve(root, glob)), types: true })),
|
|
48
57
|
);
|
|
49
58
|
const optimizedDependencies = [
|
|
50
59
|
"vue",
|
|
@@ -66,6 +75,41 @@ const optimizedDependencies = [
|
|
|
66
75
|
"json-schema-to-zod",
|
|
67
76
|
"striptags",
|
|
68
77
|
];
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Entry points the dependency optimizer crawls at startup.
|
|
81
|
+
*
|
|
82
|
+
* Every materialized module brings its own dependency set — subpath exports and
|
|
83
|
+
* transitive CommonJS included — and none of it can be listed in
|
|
84
|
+
* `optimizedDependencies` by hand, because the modules are only known at
|
|
85
|
+
* materialization time. Leaving the crawl off and letting Vite discover them as
|
|
86
|
+
* pages load meant the first visit to a cold page re-ran the optimizer
|
|
87
|
+
* mid-request: the chunks already in flight answered 504 and the client was
|
|
88
|
+
* told to reload the whole document. Crawling the module sources once, at
|
|
89
|
+
* startup, pays that cost a single time and in a place where it reads as
|
|
90
|
+
* startup rather than as a crash.
|
|
91
|
+
*
|
|
92
|
+
* Normalized for the same reason as `importDirectories`: Vite hands these
|
|
93
|
+
* patterns straight to its glob matcher without touching the separators.
|
|
94
|
+
*/
|
|
95
|
+
const optimizerEntries = [
|
|
96
|
+
resolve(__dirname, "main.ts"),
|
|
97
|
+
...moduleRoots.map((root) => resolve(root, "dms.frontend.ts")),
|
|
98
|
+
...frontendSourceRoots.flatMap((root) => [
|
|
99
|
+
resolve(root, "app/**/*.vue"),
|
|
100
|
+
resolve(root, "app/**/*.ts"),
|
|
101
|
+
]),
|
|
102
|
+
].map(normalizePath);
|
|
103
|
+
|
|
104
|
+
function paintBeforeHydrate(): Plugin {
|
|
105
|
+
return {
|
|
106
|
+
name: "dms-paint-before-hydrate",
|
|
107
|
+
apply: "build",
|
|
108
|
+
enforce: "post",
|
|
109
|
+
transformIndexHtml: { order: "post", handler: orderHeadForFirstPaint },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
69
113
|
const uiLinkImport = "@nuxt/ui/components/Link.vue";
|
|
70
114
|
const uiInertiaLinkImport = resolve(
|
|
71
115
|
__dirname,
|
|
@@ -92,7 +136,13 @@ export default defineConfig({
|
|
|
92
136
|
imports: [
|
|
93
137
|
"vue",
|
|
94
138
|
{
|
|
95
|
-
|
|
139
|
+
// This key is inlined verbatim as the module specifier of the
|
|
140
|
+
// import the auto-importer prepends to each file. A native Windows
|
|
141
|
+
// path would land inside a single-quoted JavaScript string with its
|
|
142
|
+
// backslashes intact, where `\U`, `\M` and `\f` are read as escape
|
|
143
|
+
// sequences: the specifier the bundler then resolves is a mangled
|
|
144
|
+
// path that cannot exist. Keep it POSIX.
|
|
145
|
+
[normalizePath(resolve(__dirname, "frontend-module.ts"))]: [
|
|
96
146
|
"$fetch",
|
|
97
147
|
"abortNavigation",
|
|
98
148
|
"clearError",
|
|
@@ -131,6 +181,7 @@ export default defineConfig({
|
|
|
131
181
|
vueTemplate: true,
|
|
132
182
|
},
|
|
133
183
|
}),
|
|
184
|
+
paintBeforeHydrate(),
|
|
134
185
|
],
|
|
135
186
|
resolve: {
|
|
136
187
|
dedupe: ["vue", "reka-ui", "@nuxt/ui"],
|
|
@@ -152,8 +203,7 @@ export default defineConfig({
|
|
|
152
203
|
},
|
|
153
204
|
optimizeDeps: {
|
|
154
205
|
include: optimizedDependencies,
|
|
155
|
-
|
|
156
|
-
entries: [],
|
|
206
|
+
entries: optimizerEntries,
|
|
157
207
|
},
|
|
158
208
|
ssr: { noExternal: ["@nuxt/icon", "@nuxt/ui"] },
|
|
159
209
|
server: { strictPort: true, allowedHosts: [".onamp.dev"] },
|
|
@@ -2,7 +2,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { basename, resolve } from "node:path";
|
|
3
3
|
import vue from "@vitejs/plugin-vue";
|
|
4
4
|
import AutoImport from "unplugin-auto-import/vite";
|
|
5
|
-
import { defineConfig } from "vite";
|
|
5
|
+
import { defineConfig, normalizePath } from "vite";
|
|
6
6
|
|
|
7
7
|
interface FrontendModuleRegistryEntry {
|
|
8
8
|
id: string;
|
|
@@ -55,7 +55,10 @@ export default defineConfig({
|
|
|
55
55
|
dts: false,
|
|
56
56
|
imports: [
|
|
57
57
|
{
|
|
58
|
-
|
|
58
|
+
// Inlined verbatim as a module specifier in the generated import, so
|
|
59
|
+
// it has to be POSIX: a native Windows path would reach the parser
|
|
60
|
+
// with its backslashes read as string escapes. See vite.config.ts.
|
|
61
|
+
[normalizePath(resolve(__dirname, "email-runtime.ts"))]: [
|
|
59
62
|
"useDmsAppConfig",
|
|
60
63
|
"useDmsRuntimeConfig",
|
|
61
64
|
"defineAppConfig",
|