@antelopejs/dms-frontend 0.7.1 → 0.9.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 +4 -4
- package/dist/commands/build-action.d.ts +8 -5
- package/dist/commands/build-action.js +16 -41
- package/dist/commands/build.js +9 -15
- package/dist/commands/dev-action.d.ts +2 -0
- package/dist/commands/dev-action.js +14 -3
- package/dist/commands/dev.js +3 -1
- package/dist/commands/start-action.d.ts +2 -0
- package/dist/commands/start-action.js +65 -47
- package/dist/commands/start.js +8 -6
- package/dist/common.d.ts +1 -0
- package/dist/common.js +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +9 -0
- package/dist/derived-outputs.d.ts +1 -5
- package/dist/derived-outputs.js +48 -35
- package/dist/dev-modules.d.ts +5 -0
- package/dist/dev-modules.js +88 -0
- package/dist/entry-weight.d.ts +24 -0
- package/dist/entry-weight.js +86 -0
- package/dist/fs-sync.d.ts +1 -0
- package/dist/fs-sync.js +6 -0
- package/dist/help.js +12 -4
- package/dist/index.js +4 -6
- package/dist/layer-watch.js +7 -2
- package/dist/layers.d.ts +2 -3
- package/dist/layers.js +1 -95
- package/dist/locale-files.d.ts +6 -0
- package/dist/locale-files.js +75 -0
- package/dist/manifest.d.ts +8 -8
- package/dist/manifest.js +17 -39
- package/dist/materialize.js +6 -0
- package/dist/module-set.d.ts +14 -12
- package/dist/module-set.js +39 -31
- package/dist/private-values.d.ts +3 -2
- package/dist/private-values.js +16 -5
- package/dist/project-modules-child.d.ts +13 -0
- package/dist/project-modules-child.js +26 -0
- package/dist/project-modules.d.ts +47 -0
- package/dist/project-modules.js +268 -0
- package/dist/workspace-setup.d.ts +3 -7
- package/dist/workspace-setup.js +79 -62
- package/dist/workspace.d.ts +8 -4
- package/dist/workspace.js +1 -1
- package/package.json +13 -8
- package/skills/dms-frontend/SKILL.md +2 -2
- package/templates/vue/app-runtime.ts +8 -1
- package/templates/vue/client-chunking.mjs +61 -0
- package/templates/vue/email-renderer.ts +6 -1
- package/templates/vue/entry-weight.mjs +132 -0
- package/templates/vue/frontend-build-loader.ts +1 -1
- package/templates/vue/frontend-module.ts +168 -1
- package/templates/vue/gitignore +1 -0
- package/templates/vue/main.ts +32 -2
- package/templates/vue/network-error.ts +3 -1
- package/templates/vue/package.json +3 -3
- package/templates/vue/patches/@vueuse__core@14.4.0.patch +24 -0
- package/templates/vue/pnpm-workspace.yaml +5 -0
- package/templates/vue/prepare-types.mjs +2 -7
- package/templates/vue/public-options.ts +19 -0
- package/templates/vue/server/auth/backend.mjs +20 -3
- package/templates/vue/server/auth/client-ip.mjs +64 -22
- package/templates/vue/server/auth/session.mjs +4 -11
- package/templates/vue/server/backend-proxies.mjs +13 -2
- package/templates/vue/server/backend-response.mjs +2 -13
- package/templates/vue/server/cli-report.mjs +42 -18
- package/templates/vue/server/client-manifest.mjs +34 -58
- package/templates/vue/server/dev-private-files.mjs +1 -1
- package/templates/vue/server/dev-styles.mjs +16 -5
- package/templates/vue/server/homepage.mjs +4 -1
- package/templates/vue/server/in-place-render.mjs +110 -0
- package/templates/vue/server/module-event.mjs +23 -8
- package/templates/vue/server/module-server.mjs +4 -4
- package/templates/vue/server/module-testing.mjs +5 -3
- package/templates/vue/server/page-props.mjs +10 -0
- package/templates/vue/server/proxy-compression.mjs +15 -8
- package/templates/vue/server/proxy-headers.mjs +12 -1
- package/templates/vue/server/proxy-redirects.mjs +66 -0
- package/templates/vue/server/proxy-transfers.mjs +110 -0
- package/templates/vue/server/public-endpoints.mjs +2 -2
- package/templates/vue/server/public-options.mjs +49 -0
- package/templates/vue/server/request-routing.mjs +20 -0
- package/templates/vue/server/web-body.mjs +24 -0
- package/templates/vue/server-module.ts +19 -7
- package/templates/vue/server.mjs +98 -78
- package/templates/vue/sfc-global-types.mjs +119 -0
- package/templates/vue/ssr-renderer.ts +27 -1
- package/templates/vue/ui-kbd.mjs +45 -0
- package/templates/vue/vite.config.ts +104 -47
- package/templates/vue/vite.email.config.ts +6 -3
- package/templates/vue/vite.server.config.ts +5 -2
- package/dist/commands/snapshot-action.d.ts +0 -6
- package/dist/commands/snapshot-action.js +0 -53
- package/dist/commands/snapshot.d.ts +0 -2
- package/dist/commands/snapshot.js +0 -55
- package/dist/snapshot.d.ts +0 -25
- package/dist/snapshot.js +0 -90
- package/templates/vue/server/backend-wake.mjs +0 -129
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// How the client bundle is cut into files, and what the build says about it.
|
|
2
|
+
//
|
|
3
|
+
// Kept beside vite.config.ts rather than inside it so the options can be
|
|
4
|
+
// exercised on their own, the way head-order.mjs and client-server-guard.mjs are.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Rolldown's `output.codeSplitting` for the client build.
|
|
8
|
+
*
|
|
9
|
+
* By default Rolldown gives every set of dynamic imports that shares a module
|
|
10
|
+
* its own common chunk. The DMS's pages are hundreds of dynamic imports (a
|
|
11
|
+
* module's components and pages are `import.meta.glob`ed), so the code the
|
|
12
|
+
* entry also needs lands in about sixty small common chunks the entry imports
|
|
13
|
+
* before anything runs: one request each, 55 to 60 more files on every page
|
|
14
|
+
* than the single entry file Rollup made, for no byte saved.
|
|
15
|
+
*
|
|
16
|
+
* The `$initial` tag is every module the entry imports statically, directly or
|
|
17
|
+
* not. One group over it puts them in the entry file again, and leaves every
|
|
18
|
+
* lazy chunk to Rolldown's own splitting, which a page only loads when it needs
|
|
19
|
+
* it: what an anonymous page loads is still what that page's own routes pull.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately not here: groups over what the lazy chunks share (the UI
|
|
22
|
+
* libraries, a chunk per module). They cut a signed-in page's files by a
|
|
23
|
+
* quarter to four fifths, but every group is loaded whole by each page that
|
|
24
|
+
* uses any of it, and the anonymous pages pay 70 to 115 KB of Brotli for code
|
|
25
|
+
* only the signed-in shell uses.
|
|
26
|
+
*/
|
|
27
|
+
export const clientCodeSplitting = {
|
|
28
|
+
groups: [{ name: "entry", tags: ["$initial"] }],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const MODULE_ENTRY = /[\\/]dms\.frontend\.[cm]?[jt]sx?$/;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Rolldown's `onLog` for every build: drops the
|
|
35
|
+
* INEFFECTIVE_DYNAMIC_IMPORT notices whose dynamic importer is a module's
|
|
36
|
+
* `dms.frontend.ts`.
|
|
37
|
+
*
|
|
38
|
+
* That file `import.meta.glob`s every component of the module so a page can
|
|
39
|
+
* resolve one by name. A few of them (StatusPill, MonoCell, IdentityCell: the
|
|
40
|
+
* table cell renderers) are also imported statically by the entry, which is
|
|
41
|
+
* what a cell renderer needs at hydration. The glob entry then resolves to the
|
|
42
|
+
* chunk already loaded: nothing is duplicated and nothing was meant to be
|
|
43
|
+
* lazy, and nobody can act on the notice. A `import()` a module author
|
|
44
|
+
* writes by hand for a file that is also imported statically is the same
|
|
45
|
+
* mistake with a different cause (the lazy load never happens), and still
|
|
46
|
+
* warns.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} level
|
|
49
|
+
* @param {{ code?: string, ids?: string[] }} log
|
|
50
|
+
* @param {(level: string, log: object) => void} defaultHandler
|
|
51
|
+
*/
|
|
52
|
+
export function onBuildLog(level, log, defaultHandler) {
|
|
53
|
+
if (
|
|
54
|
+
log.code === "INEFFECTIVE_DYNAMIC_IMPORT" &&
|
|
55
|
+
log.ids?.length &&
|
|
56
|
+
MODULE_ENTRY.test(log.ids[0])
|
|
57
|
+
) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
defaultHandler(level, log);
|
|
61
|
+
}
|
|
@@ -6,6 +6,7 @@ import { appConfigs } from "./app-configs.generated";
|
|
|
6
6
|
import moduleRegistry from "./generated-frontend-modules.json";
|
|
7
7
|
import { loadEmailLocaleMessages } from "./email-locales";
|
|
8
8
|
import { stripSsrMarkers } from "./email-markup";
|
|
9
|
+
import { parsePublicOptions } from "./public-options";
|
|
9
10
|
|
|
10
11
|
interface EmailModule {
|
|
11
12
|
default: Component;
|
|
@@ -20,6 +21,8 @@ interface EmailRenderOptions {
|
|
|
20
21
|
locale?: string;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
const runtimeOptions = parsePublicOptions(process.env.DMS_PUBLIC_OPTIONS);
|
|
25
|
+
|
|
23
26
|
const registries = import.meta.glob<EmailRegistry>(
|
|
24
27
|
"/frontend-modules/*/dms.email.ts",
|
|
25
28
|
{ eager: true },
|
|
@@ -157,7 +160,9 @@ export async function renderEmail(
|
|
|
157
160
|
app.provide("dmsEmailAppConfig", defu({}, ...emailAppConfigs));
|
|
158
161
|
const publicConfig = defu(
|
|
159
162
|
{},
|
|
160
|
-
...moduleRegistry.modules.map(
|
|
163
|
+
...moduleRegistry.modules.map(
|
|
164
|
+
(module) => runtimeOptions?.[module.id] ?? module.options,
|
|
165
|
+
),
|
|
161
166
|
);
|
|
162
167
|
app.provide("dmsEmailRuntimeConfig", {
|
|
163
168
|
public: defu(
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// What each frontend module adds to the client entry.
|
|
2
|
+
//
|
|
3
|
+
// Every page loads the entry, and everything it imports statically, before it
|
|
4
|
+
// runs anything: a module's `dms.frontend.ts`, its plugins and whatever they
|
|
5
|
+
// import land there, on every page of every application that loads the
|
|
6
|
+
// module. The client build records, from the bundle's own metadata, how much
|
|
7
|
+
// of the entry each module's sources make up; `ajs dms build` reports it and
|
|
8
|
+
// warns about a module above its threshold.
|
|
9
|
+
//
|
|
10
|
+
// Kept beside vite.config.ts rather than inside it so the plugin can be
|
|
11
|
+
// exercised on its own, the way client-chunking.mjs is.
|
|
12
|
+
|
|
13
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { dirname, join, relative, sep } from "node:path";
|
|
15
|
+
|
|
16
|
+
/** Where the client build writes the attribution, under `dist/client`. */
|
|
17
|
+
export const ENTRY_WEIGHT_FILE = ".vite/entry-weight.json";
|
|
18
|
+
|
|
19
|
+
/** Owners of the entry's bytes that are no frontend module. */
|
|
20
|
+
export const SHARED_OWNER = "shared";
|
|
21
|
+
export const TRANSLATIONS_OWNER = "translations";
|
|
22
|
+
|
|
23
|
+
const LOCALE_CATALOG = /[\\/]locales\.generated[\\/]/;
|
|
24
|
+
|
|
25
|
+
function stripQuery(id) {
|
|
26
|
+
const query = id.indexOf("?");
|
|
27
|
+
return query === -1 ? id : id.slice(0, query);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isInside(root, file) {
|
|
31
|
+
const path = relative(root, file);
|
|
32
|
+
return Boolean(path) && !path.startsWith("..") && !path.startsWith(sep);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The owner of a module id of the bundle: the frontend module whose sources
|
|
37
|
+
* hold the file, by its manifest name; the merged translation catalogs; else
|
|
38
|
+
* what every page shares (the renderer's runtime, the UI and icon libraries,
|
|
39
|
+
* an installed package).
|
|
40
|
+
*
|
|
41
|
+
* @param {{ root: string, name?: string, packageName?: string, id: string }[]} modules
|
|
42
|
+
*/
|
|
43
|
+
export function entryOwner(id, modules) {
|
|
44
|
+
if (id.startsWith("\0")) return SHARED_OWNER;
|
|
45
|
+
const file = stripQuery(id);
|
|
46
|
+
if (/[\\/]node_modules[\\/]/.test(file)) return SHARED_OWNER;
|
|
47
|
+
if (LOCALE_CATALOG.test(file)) return TRANSLATIONS_OWNER;
|
|
48
|
+
const module = modules.find(({ root }) => isInside(root, file));
|
|
49
|
+
return module
|
|
50
|
+
? (module.name ?? module.packageName ?? module.id)
|
|
51
|
+
: SHARED_OWNER;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The files a page loads before its first line runs: the entry chunks and
|
|
56
|
+
* their static imports, transitively, by file name.
|
|
57
|
+
*/
|
|
58
|
+
function entryClosure(bundle) {
|
|
59
|
+
const chunks = Object.values(bundle).filter(
|
|
60
|
+
(output) => output.type === "chunk",
|
|
61
|
+
);
|
|
62
|
+
const byName = new Map(chunks.map((chunk) => [chunk.fileName, chunk]));
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
const visit = (chunk) => {
|
|
65
|
+
if (!chunk || seen.has(chunk.fileName)) return;
|
|
66
|
+
seen.add(chunk.fileName);
|
|
67
|
+
for (const file of chunk.imports ?? []) visit(byName.get(file));
|
|
68
|
+
};
|
|
69
|
+
for (const chunk of chunks) if (chunk.isEntry) visit(chunk);
|
|
70
|
+
return [...seen].map((file) => byName.get(file));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The bytes of the entry each owner accounts for. Each chunk's bytes are
|
|
75
|
+
* shared out among the modules it holds in proportion to the code Rolldown
|
|
76
|
+
* rendered for each (`renderedLength`, before minification): an estimate that
|
|
77
|
+
* needs nothing the bundle does not already hold.
|
|
78
|
+
*
|
|
79
|
+
* @returns {{ files: string[], bytes: number, owners: Record<string, number> }}
|
|
80
|
+
*/
|
|
81
|
+
export function attributeEntry(bundle, modules) {
|
|
82
|
+
const files = entryClosure(bundle);
|
|
83
|
+
const owners = {};
|
|
84
|
+
let bytes = 0;
|
|
85
|
+
for (const chunk of files) {
|
|
86
|
+
const size = Buffer.byteLength(chunk.code);
|
|
87
|
+
bytes += size;
|
|
88
|
+
const rendered = Object.entries(chunk.modules ?? {});
|
|
89
|
+
const total = rendered.reduce(
|
|
90
|
+
(sum, [, info]) => sum + info.renderedLength,
|
|
91
|
+
0,
|
|
92
|
+
);
|
|
93
|
+
if (!total) {
|
|
94
|
+
owners[SHARED_OWNER] = (owners[SHARED_OWNER] ?? 0) + size;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
for (const [id, info] of rendered) {
|
|
98
|
+
const owner = entryOwner(id, modules);
|
|
99
|
+
owners[owner] =
|
|
100
|
+
(owners[owner] ?? 0) + (size * info.renderedLength) / total;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (const owner of Object.keys(owners))
|
|
104
|
+
owners[owner] = Math.round(owners[owner]);
|
|
105
|
+
return { files: files.map((chunk) => chunk.fileName), bytes, owners };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Records, in the client build, what each frontend module adds to the entry
|
|
110
|
+
* (`ENTRY_WEIGHT_FILE`). Server builds are left alone: their bundle is no
|
|
111
|
+
* page's entry.
|
|
112
|
+
*/
|
|
113
|
+
export function recordEntryWeight(modules) {
|
|
114
|
+
let ssrBuild = false;
|
|
115
|
+
return {
|
|
116
|
+
name: "dms-entry-weight",
|
|
117
|
+
configResolved(config) {
|
|
118
|
+
ssrBuild = Boolean(config.build.ssr);
|
|
119
|
+
},
|
|
120
|
+
// Once the bundle is written: plugins that generate the bundle after this
|
|
121
|
+
// one still rewrite the chunks' code.
|
|
122
|
+
writeBundle(options, bundle) {
|
|
123
|
+
if (ssrBuild || !options.dir) return;
|
|
124
|
+
const file = join(options.dir, ENTRY_WEIGHT_FILE);
|
|
125
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
126
|
+
writeFileSync(
|
|
127
|
+
file,
|
|
128
|
+
`${JSON.stringify(attributeEntry(bundle, modules), null, 2)}\n`,
|
|
129
|
+
);
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isAbsolute, posix, resolve } from "node:path";
|
|
2
2
|
import { normalizePath } from "vite";
|
|
3
|
-
import type { DmsFrontendBuildSetup } from "./frontend-build";
|
|
3
|
+
import type { DmsFrontendBuildSetup } from "./frontend-build.ts";
|
|
4
4
|
|
|
5
5
|
/** A module's `dms.frontend.build.ts`, as the generated loader imports it. */
|
|
6
6
|
export interface FrontendBuildDeclaration {
|
|
@@ -12,7 +12,9 @@ import { defu } from "defu";
|
|
|
12
12
|
import { type FetchOptions, ofetch } from "ofetch";
|
|
13
13
|
import {
|
|
14
14
|
type App,
|
|
15
|
+
type AppConfig,
|
|
15
16
|
type Component,
|
|
17
|
+
type ComponentInternalInstance,
|
|
16
18
|
type ComponentPublicInstance,
|
|
17
19
|
type ComputedRef,
|
|
18
20
|
camelize,
|
|
@@ -55,6 +57,7 @@ import {
|
|
|
55
57
|
mergeLayerAppConfigs,
|
|
56
58
|
withAppConfigChanges,
|
|
57
59
|
} from "./layer-app-config";
|
|
60
|
+
import type { DmsPublicOptionsById } from "./public-options";
|
|
58
61
|
|
|
59
62
|
export interface DmsPageRoute {
|
|
60
63
|
displayName?: string;
|
|
@@ -457,11 +460,34 @@ export interface DmsFrontendModule {
|
|
|
457
460
|
setup(sdk: DmsFrontendSdk): void | Promise<void>;
|
|
458
461
|
}
|
|
459
462
|
export interface DmsFrontendModuleRegistration {
|
|
463
|
+
/** The module's id in the generated module registry. */
|
|
464
|
+
id?: string;
|
|
460
465
|
/** The module's manifest name, which collision warnings name it by. */
|
|
461
466
|
name?: string;
|
|
462
467
|
module: DmsFrontendModule;
|
|
463
468
|
options: DmsModuleOptions;
|
|
464
469
|
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* The registrations with the public options the server runs with in place of
|
|
473
|
+
* the build's: `ajs dms start` computes them from the project's configuration
|
|
474
|
+
* and the deployment's variables. Unset, the build's options stand.
|
|
475
|
+
*/
|
|
476
|
+
export function withPublicOptions(
|
|
477
|
+
registrations: DmsFrontendModuleRegistration[],
|
|
478
|
+
byId: DmsPublicOptionsById | undefined,
|
|
479
|
+
): DmsFrontendModuleRegistration[] {
|
|
480
|
+
if (!byId) return registrations;
|
|
481
|
+
return registrations.map((registration) => {
|
|
482
|
+
const options = registration.id ? byId[registration.id] : undefined;
|
|
483
|
+
return options
|
|
484
|
+
? {
|
|
485
|
+
...registration,
|
|
486
|
+
options: { ...registration.options, public: options },
|
|
487
|
+
}
|
|
488
|
+
: registration;
|
|
489
|
+
});
|
|
490
|
+
}
|
|
465
491
|
/** A layer's `app/app.config.ts`, as `app-configs.generated.ts` lists them. */
|
|
466
492
|
export interface DmsLayerAppConfig {
|
|
467
493
|
/** The id of the frontend module the layer belongs to. */
|
|
@@ -1829,6 +1855,9 @@ function startAsyncData<T>(
|
|
|
1829
1855
|
const idle = entry.result.status.value === "idle";
|
|
1830
1856
|
if (runtime.isServer) {
|
|
1831
1857
|
if (!immediate || !onServer) return undefined;
|
|
1858
|
+
// A render that already answers a redirect throws its HTML away: what it
|
|
1859
|
+
// still has to read would reach nobody.
|
|
1860
|
+
if (runtime.serverRedirect) return undefined;
|
|
1832
1861
|
if (idle && options.lazy) entry.schedule();
|
|
1833
1862
|
else if (idle) void entry.load();
|
|
1834
1863
|
const settled = entry.settled();
|
|
@@ -2464,6 +2493,35 @@ export function defineDmsPageMeta(meta: Record<string, unknown>): void {
|
|
|
2464
2493
|
onServerPrefetch(() => navigation.then(ignoreSettlement, ignoreSettlement));
|
|
2465
2494
|
}
|
|
2466
2495
|
function ignoreSettlement(): void {}
|
|
2496
|
+
/**
|
|
2497
|
+
* What Vue's development build passes `errorHandler` for an error thrown by a
|
|
2498
|
+
* component's `setup`, sync or async (production passes a link instead; it has
|
|
2499
|
+
* no warnings to scope either).
|
|
2500
|
+
*/
|
|
2501
|
+
const SETUP_FUNCTION_ERROR = "setup function";
|
|
2502
|
+
/**
|
|
2503
|
+
* Whether a Vue warning comes from a component whose `setup` failed, or from
|
|
2504
|
+
* under it. Vue still renders such a component, against the setup state that
|
|
2505
|
+
* never came: in development, where a single-file component's template is
|
|
2506
|
+
* compiled apart from its setup, the template reads the missing bindings, and
|
|
2507
|
+
* every component it uses becomes `undefined` ("Invalid vnode type when
|
|
2508
|
+
* creating vnode: undefined", then "Component <Anonymous> is missing template
|
|
2509
|
+
* or render function" for the comment Vue renders in its place). The error
|
|
2510
|
+
* page replaces that output (a server renders the page again, the browser
|
|
2511
|
+
* swaps it), and the failure itself is already reported.
|
|
2512
|
+
*
|
|
2513
|
+
* Walks internal instances: a `<script setup>` component exposes itself, so
|
|
2514
|
+
* its children's `$parent` is an expose proxy, not the proxy `errorHandler`
|
|
2515
|
+
* received.
|
|
2516
|
+
*/
|
|
2517
|
+
function withinFailedSetup(
|
|
2518
|
+
instance: ComponentInternalInstance | null,
|
|
2519
|
+
failedSetups: WeakSet<ComponentInternalInstance>,
|
|
2520
|
+
): boolean {
|
|
2521
|
+
for (let current = instance; current; current = current.parent)
|
|
2522
|
+
if (failedSetups.has(current)) return true;
|
|
2523
|
+
return false;
|
|
2524
|
+
}
|
|
2467
2525
|
/** Shows the error page in place of the page, keeping the first failure. */
|
|
2468
2526
|
function failPage(runtime: DmsFrontendRuntime, reason: unknown): void {
|
|
2469
2527
|
runtime.currentError.value ??=
|
|
@@ -2554,6 +2612,80 @@ export const DmsLink: Component = defineComponent({
|
|
|
2554
2612
|
},
|
|
2555
2613
|
});
|
|
2556
2614
|
|
|
2615
|
+
/** How a keyboard hint prints its modifiers: macOS symbols, or PC key names. */
|
|
2616
|
+
export type DmsKbdPlatform = "mac" | "other";
|
|
2617
|
+
/**
|
|
2618
|
+
* Where Nuxt UI's key hints read the platform: called in the setup of each
|
|
2619
|
+
* component that prints one, it returns the platform that component renders.
|
|
2620
|
+
* It must read on the server the platform it renders there, and keep it while
|
|
2621
|
+
* its caller hydrates, so the browser's first render matches the server's.
|
|
2622
|
+
*/
|
|
2623
|
+
export type DmsKbdPlatformSource = () => Readonly<Ref<DmsKbdPlatform>>;
|
|
2624
|
+
|
|
2625
|
+
const KBD_PLATFORM_KEY: InjectionKey<DmsKbdPlatformSource> =
|
|
2626
|
+
Symbol("dms:kbd-platform");
|
|
2627
|
+
const MODIFIER_KBD_KEYS: Record<
|
|
2628
|
+
DmsKbdPlatform,
|
|
2629
|
+
Record<"meta" | "ctrl" | "alt", string>
|
|
2630
|
+
> = {
|
|
2631
|
+
mac: { meta: "⌘", ctrl: "⌃", alt: "⌥" },
|
|
2632
|
+
other: { meta: "Ctrl", ctrl: "Ctrl", alt: "Alt" },
|
|
2633
|
+
};
|
|
2634
|
+
const isModifierKbdKey = (value: string): value is "meta" | "ctrl" | "alt" =>
|
|
2635
|
+
value === "meta" || value === "ctrl" || value === "alt";
|
|
2636
|
+
|
|
2637
|
+
/**
|
|
2638
|
+
* Gives Nuxt UI's key hints (`UKbd`, the `kbds` of its menus, the dashboard
|
|
2639
|
+
* search button) the application's keyboard platform, so the server prints
|
|
2640
|
+
* `meta` as "⌘" or "Ctrl" from the first render instead of a blank the
|
|
2641
|
+
* browser fills in after mounting.
|
|
2642
|
+
*/
|
|
2643
|
+
export function provideDmsKbdPlatform(
|
|
2644
|
+
app: App,
|
|
2645
|
+
source: DmsKbdPlatformSource,
|
|
2646
|
+
): void {
|
|
2647
|
+
app.provide(KBD_PLATFORM_KEY, source);
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
interface NuxtUiKbd {
|
|
2651
|
+
macOS: ComputedRef<unknown>;
|
|
2652
|
+
getKbdKey: (value?: string) => string | undefined;
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
/**
|
|
2656
|
+
* Nuxt UI's `useKbd` as the DMS runs it (`ui-kbd.mjs` routes it here): a hint
|
|
2657
|
+
* renders the same on the server and in the hydrating browser. Nuxt UI prints
|
|
2658
|
+
* `meta`, `ctrl` and `alt` from a map shared by every hint and filled by the
|
|
2659
|
+
* first one's `onMounted`, so a hint hydrating after that (in an async
|
|
2660
|
+
* component) printed "⌘" over the server's blank. Each hint now keeps the
|
|
2661
|
+
* server's value until its own `onMounted`; with a platform provided
|
|
2662
|
+
* (`provideDmsKbdPlatform`), that value is the platform's from the start.
|
|
2663
|
+
*
|
|
2664
|
+
* @internal
|
|
2665
|
+
*/
|
|
2666
|
+
export function useDmsKbd(
|
|
2667
|
+
shared: () => NuxtUiKbd,
|
|
2668
|
+
keys: Readonly<Record<string, string>>,
|
|
2669
|
+
): NuxtUiKbd {
|
|
2670
|
+
const { macOS } = shared();
|
|
2671
|
+
const instance = getCurrentInstance();
|
|
2672
|
+
const source = instance ? inject(KBD_PLATFORM_KEY, null) : null;
|
|
2673
|
+
const platform = source ? source() : null;
|
|
2674
|
+
// A hydrating component's vnode is bound to its server node before setup.
|
|
2675
|
+
const hydrating = ref(!!instance?.vnode.el);
|
|
2676
|
+
if (hydrating.value) onMounted(() => (hydrating.value = false));
|
|
2677
|
+
return {
|
|
2678
|
+
macOS,
|
|
2679
|
+
getKbdKey(value) {
|
|
2680
|
+
if (!value) return undefined;
|
|
2681
|
+
if (!isModifierKbdKey(value)) return keys[value] || value;
|
|
2682
|
+
if (platform) return MODIFIER_KBD_KEYS[platform.value][value];
|
|
2683
|
+
if (typeof window === "undefined" || hydrating.value) return " ";
|
|
2684
|
+
return MODIFIER_KBD_KEYS[macOS.value ? "mac" : "other"][value];
|
|
2685
|
+
},
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2557
2689
|
export const DmsClientOnly = defineComponent({
|
|
2558
2690
|
name: "DmsClientOnly",
|
|
2559
2691
|
setup(_, { slots }) {
|
|
@@ -2793,6 +2925,17 @@ export function useDmsPageLoading(): Readonly<Ref<boolean>> {
|
|
|
2793
2925
|
return readonly(useDmsRuntime().pageLoading);
|
|
2794
2926
|
}
|
|
2795
2927
|
|
|
2928
|
+
/**
|
|
2929
|
+
* Whether the browser is hydrating the page the server rendered, whose first
|
|
2930
|
+
* render must match the server's HTML: from the start of the application to
|
|
2931
|
+
* the tick the page's `<Suspense>` resolves. False on the server, in a page
|
|
2932
|
+
* the server did not render, and after a client navigation. As Nuxt's
|
|
2933
|
+
* `nuxtApp.isHydrating`; read once, it is not reactive.
|
|
2934
|
+
*/
|
|
2935
|
+
export function isDmsHydrating(): boolean {
|
|
2936
|
+
return useDmsRuntime().hydrating;
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2796
2939
|
/**
|
|
2797
2940
|
* The props of the `<Suspense>` around the page that keep `useDmsPageLoading`
|
|
2798
2941
|
* in step with it. Cleared before the boundary unmounts, so a page left while
|
|
@@ -3344,7 +3487,31 @@ export async function installDmsPlugins(
|
|
|
3344
3487
|
if (browserRuntime === runtime) browserRuntime = undefined;
|
|
3345
3488
|
});
|
|
3346
3489
|
}
|
|
3347
|
-
|
|
3490
|
+
const failedSetups = new WeakSet<ComponentInternalInstance>();
|
|
3491
|
+
const warnOutsideFailedSetups: AppConfig["warnHandler"] = (
|
|
3492
|
+
message,
|
|
3493
|
+
instance,
|
|
3494
|
+
trace,
|
|
3495
|
+
) => {
|
|
3496
|
+
// The comment Vue renders in place of an `undefined` component has no
|
|
3497
|
+
// public instance: the template rendering it is the one that failed.
|
|
3498
|
+
const source = instance?.$ ?? getCurrentInstance();
|
|
3499
|
+
if (!withinFailedSetup(source, failedSetups))
|
|
3500
|
+
console.warn(`[Vue warn]: ${message}${trace ? `\n${trace}` : ""}`);
|
|
3501
|
+
};
|
|
3502
|
+
app.config.errorHandler = (reason, instance, info) => {
|
|
3503
|
+
if (instance && info === SETUP_FUNCTION_ERROR) {
|
|
3504
|
+
failedSetups.add(instance.$);
|
|
3505
|
+
// From the first failure on only: until then Vue keeps its own output,
|
|
3506
|
+
// whose component trace shows the props. An app that already has a
|
|
3507
|
+
// handler keeps it. A server renders a single page per app; in the
|
|
3508
|
+
// browser the app outlives Inertia visits, so once a page has failed
|
|
3509
|
+
// the handler stays for the session and later warnings print as plain
|
|
3510
|
+
// text (no inspectable objects), still all of them.
|
|
3511
|
+
app.config.warnHandler ??= warnOutsideFailedSetups;
|
|
3512
|
+
}
|
|
3513
|
+
failPage(runtime, reason);
|
|
3514
|
+
};
|
|
3348
3515
|
app.config.globalProperties.$dms = appContext;
|
|
3349
3516
|
Reflect.set(app, "$dms", appContext);
|
|
3350
3517
|
Reflect.set(app, "$i18n", i18n);
|
package/templates/vue/gitignore
CHANGED
|
@@ -10,6 +10,7 @@ frontend-modules/**/i18n/locales/
|
|
|
10
10
|
# CSS hot update on plain navigation.
|
|
11
11
|
auto-imports.d.ts
|
|
12
12
|
components.d.ts
|
|
13
|
+
sfc-global-types.generated.d.ts
|
|
13
14
|
# Build output, which the email bundle watcher rewrites during development:
|
|
14
15
|
# read as Tailwind sources, each rebuild would push a burst of CSS updates.
|
|
15
16
|
dist/
|
package/templates/vue/main.ts
CHANGED
|
@@ -9,11 +9,14 @@ import {
|
|
|
9
9
|
configureDmsApp,
|
|
10
10
|
resolveDmsInertiaPage,
|
|
11
11
|
} from "./app-runtime";
|
|
12
|
-
import { setupFrontendModules } from "./frontend-module";
|
|
12
|
+
import { setupFrontendModules, withPublicOptions } from "./frontend-module";
|
|
13
|
+
import { parsePublicOptions } from "./public-options";
|
|
13
14
|
import { frontendModules } from "./frontend-modules.generated";
|
|
14
15
|
import { sameOriginIconProvider } from "./icon-api.mjs";
|
|
15
16
|
|
|
16
17
|
const DEV_STYLE_ATTRIBUTE = "data-dms-dev-style";
|
|
18
|
+
/** Mirrors `PUBLIC_OPTIONS_ELEMENT_ID` in `server/public-options.mjs`. */
|
|
19
|
+
const PUBLIC_OPTIONS_ELEMENT_ID = "dms-public-options";
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
22
|
* Hand each development stylesheet over to Vite.
|
|
@@ -57,6 +60,26 @@ if (import.meta.env.DEV) adoptViteDevStyles();
|
|
|
57
60
|
// the generated server answers those queries from the installed collections.
|
|
58
61
|
addAPIProvider("", sameOriginIconProvider(location.origin));
|
|
59
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Shows the URL of the page the server sent, when it rendered a query the page
|
|
65
|
+
* completed in place of the one requested (`renderDocument` in server.mjs).
|
|
66
|
+
* Replaced before Inertia starts: Inertia records a first page whose URL is
|
|
67
|
+
* not the address bar's as a new history entry, and Back would then lead to
|
|
68
|
+
* the URL the page completes again.
|
|
69
|
+
*/
|
|
70
|
+
function showServedUrl(url: string): void {
|
|
71
|
+
const served = new URL(url, location.href);
|
|
72
|
+
if (
|
|
73
|
+
served.pathname === location.pathname &&
|
|
74
|
+
served.search !== location.search
|
|
75
|
+
)
|
|
76
|
+
history.replaceState(
|
|
77
|
+
history.state,
|
|
78
|
+
"",
|
|
79
|
+
`${served.pathname}${served.search}${location.hash}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
60
83
|
/**
|
|
61
84
|
* Sets the modules up, then starts the app.
|
|
62
85
|
*
|
|
@@ -67,7 +90,13 @@ addAPIProvider("", sameOriginIconProvider(location.origin));
|
|
|
67
90
|
* browser requested one by one.
|
|
68
91
|
*/
|
|
69
92
|
async function startDmsApp(): Promise<void> {
|
|
70
|
-
|
|
93
|
+
const runtimeOptions = parsePublicOptions(
|
|
94
|
+
document.getElementById(PUBLIC_OPTIONS_ELEMENT_ID)?.textContent,
|
|
95
|
+
);
|
|
96
|
+
await setupFrontendModules(
|
|
97
|
+
withPublicOptions(frontendModules, runtimeOptions),
|
|
98
|
+
appConfigs,
|
|
99
|
+
);
|
|
71
100
|
// A layer's app config edited during development reaches the page without a
|
|
72
101
|
// reload. A server render imports the new configs instead.
|
|
73
102
|
syncAppConfigs(applyLayerAppConfigs);
|
|
@@ -75,6 +104,7 @@ async function startDmsApp(): Promise<void> {
|
|
|
75
104
|
createInertiaApp({
|
|
76
105
|
resolve: resolveDmsInertiaPage,
|
|
77
106
|
async setup({ App, el, props, plugin }) {
|
|
107
|
+
showServedUrl(props.initialPage.url);
|
|
78
108
|
const root = { render: () => h(App, props) };
|
|
79
109
|
const app = el.hasAttribute("data-server-rendered")
|
|
80
110
|
? createSSRApp(root)
|
|
@@ -18,7 +18,9 @@ const NETWORK_ERROR_KEY = "error.network";
|
|
|
18
18
|
const DEFAULT_LOCALE = "en";
|
|
19
19
|
// Shipped with the renderer, since no module is guaranteed to translate the
|
|
20
20
|
// case; a module overrides them with `error.network.title` and
|
|
21
|
-
// `error.network.description` in its own locale files.
|
|
21
|
+
// `error.network.description` in its own locale files. `te()` reads the
|
|
22
|
+
// visitor's locale alone, and the generated catalogs leave these keys out of
|
|
23
|
+
// their English fill, so only that locale's own message replaces them.
|
|
22
24
|
const NETWORK_ERROR_MESSAGES: Record<
|
|
23
25
|
string,
|
|
24
26
|
Record<"title" | "description", string>
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
"@nuxt/ui": "4.10.0",
|
|
24
24
|
"@tailwindcss/typography": "^0.5.19",
|
|
25
25
|
"@unhead/vue": "^2.0.14",
|
|
26
|
-
"@vitejs/plugin-vue": "^6.0.
|
|
26
|
+
"@vitejs/plugin-vue": "^6.0.6",
|
|
27
27
|
"@vue/server-renderer": "^3.5.0",
|
|
28
|
-
"@vueuse/core": "
|
|
28
|
+
"@vueuse/core": "14.4.0",
|
|
29
29
|
"defu": "^6.1.4",
|
|
30
30
|
"devalue": "^5.9.4",
|
|
31
31
|
"ofetch": "^1.4.1",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"ufo": "^1.6.1",
|
|
35
35
|
"unplugin-auto-import": "^20.2.0",
|
|
36
36
|
"unplugin-vue-components": "^29.0.0",
|
|
37
|
-
"vite": "^
|
|
37
|
+
"vite": "^8.3.0",
|
|
38
38
|
"vue": "^3.5.0",
|
|
39
39
|
"vue-i18n": "^11.1.12"
|
|
40
40
|
},
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
diff --git a/dist/index.js b/dist/index.js
|
|
2
|
+
index 772a5e9..d433a57 100644
|
|
3
|
+
--- a/dist/index.js
|
|
4
|
+
+++ b/dist/index.js
|
|
5
|
+
@@ -2047,11 +2047,16 @@ function useColorMode(options = {}) {
|
|
6
|
+
let attributeToChange = null;
|
|
7
|
+
if (attribute === "class") {
|
|
8
|
+
const current = value.split(/\s/g);
|
|
9
|
+
+ // Patched by @antelopejs/dms-frontend until vueuse ships the fix: only
|
|
10
|
+
+ // the classes `el` lacks or still carries count as a change, so a mode
|
|
11
|
+
+ // already applied (by the server render, another instance or the
|
|
12
|
+
+ // mount re-run) skips the forced style recalculation below.
|
|
13
|
+
Object.values(modes).flatMap((i) => (i || "").split(/\s/g)).filter(Boolean).forEach((v) => {
|
|
14
|
+
- if (current.includes(v)) classesToAdd.add(v);
|
|
15
|
+
- else classesToRemove.add(v);
|
|
16
|
+
+ if (current.includes(v)) {
|
|
17
|
+
+ if (!el.classList.contains(v)) classesToAdd.add(v);
|
|
18
|
+
+ } else if (el.classList.contains(v)) classesToRemove.add(v);
|
|
19
|
+
});
|
|
20
|
+
- } else attributeToChange = {
|
|
21
|
+
+ } else if (el.getAttribute(attribute) !== value) attributeToChange = {
|
|
22
|
+
key: attribute,
|
|
23
|
+
value
|
|
24
|
+
};
|
|
@@ -9,3 +9,8 @@ patchedDependencies:
|
|
|
9
9
|
# Menu and breadcrumb links active on a sibling route (`/test` on `/test-a`):
|
|
10
10
|
# nuxt/ui#6803, fixed by nuxt/ui#6809.
|
|
11
11
|
"@nuxt/ui@4.10.0": patches/@nuxt__ui@4.10.0.patch
|
|
12
|
+
# useColorMode re-applies a color mode `<html>` already carries, forcing a
|
|
13
|
+
# full style recalculation on every call (6 on the dashboard home, 216 ms of
|
|
14
|
+
# main thread at 4x CPU): the guard of vueuse#4001 compares the modes with the
|
|
15
|
+
# requested value instead of the element. vueuse 15.0.0 still ships it.
|
|
16
|
+
"@vueuse/core@14.4.0": patches/@vueuse__core@14.4.0.patch
|
|
@@ -23,12 +23,7 @@ await build({
|
|
|
23
23
|
build: {
|
|
24
24
|
write: false,
|
|
25
25
|
emptyOutDir: false,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// The entry is empty on purpose.
|
|
29
|
-
onwarn: (warning, warn) => {
|
|
30
|
-
if (warning.code !== "EMPTY_BUNDLE") warn(warning);
|
|
31
|
-
},
|
|
32
|
-
},
|
|
26
|
+
// The entry is empty on purpose; Rolldown does not warn about it.
|
|
27
|
+
rolldownOptions: { input: ENTRY },
|
|
33
28
|
},
|
|
34
29
|
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Public options by module id, as `ajs dms start` computes them. */
|
|
2
|
+
export type DmsPublicOptionsById = Record<string, Record<string, unknown>>;
|
|
3
|
+
|
|
4
|
+
/** `DMS_PUBLIC_OPTIONS` as options by module id, undefined when unset. */
|
|
5
|
+
export function parsePublicOptions(
|
|
6
|
+
raw: string | null | undefined,
|
|
7
|
+
): DmsPublicOptionsById | undefined {
|
|
8
|
+
if (!raw) return undefined;
|
|
9
|
+
try {
|
|
10
|
+
const parsed: unknown = JSON.parse(raw);
|
|
11
|
+
return typeof parsed === "object" &&
|
|
12
|
+
parsed !== null &&
|
|
13
|
+
!Array.isArray(parsed)
|
|
14
|
+
? (parsed as DmsPublicOptionsById)
|
|
15
|
+
: undefined;
|
|
16
|
+
} catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|