@antelopejs/dms-frontend 0.0.1
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/LICENSE +190 -0
- package/README.md +131 -0
- package/dist/commands/build.js +66 -0
- package/dist/commands/clean.js +42 -0
- package/dist/commands/dev.js +116 -0
- package/dist/commands/prepare.js +60 -0
- package/dist/commands/start.js +49 -0
- package/dist/commands/verify-source.js +39 -0
- package/dist/common.js +24 -0
- package/dist/config.js +142 -0
- package/dist/discovery.js +123 -0
- package/dist/fs-sync.js +142 -0
- package/dist/index.js +44 -0
- package/dist/layer-watch.js +154 -0
- package/dist/layers.js +120 -0
- package/dist/manifest.js +109 -0
- package/dist/materialize.js +249 -0
- package/dist/ports.js +30 -0
- package/dist/update-check.js +173 -0
- package/dist/utils/cli-ui.js +178 -0
- package/dist/verify-source-runner.js +228 -0
- package/dist/workspace-setup.js +109 -0
- package/dist/workspace.js +76 -0
- package/package.json +97 -0
- package/templates/vue/DmsDynamicPage.vue +89 -0
- package/templates/vue/app-config-stub.mjs +1 -0
- package/templates/vue/app-runtime.ts +240 -0
- package/templates/vue/compress-assets.mjs +48 -0
- package/templates/vue/email-locales.ts +32 -0
- package/templates/vue/email-renderer.ts +159 -0
- package/templates/vue/email-runtime.ts +23 -0
- package/templates/vue/frontend-module.ts +1418 -0
- package/templates/vue/globals.d.ts +1 -0
- package/templates/vue/index.html +24 -0
- package/templates/vue/main.ts +33 -0
- package/templates/vue/npmrc +2 -0
- package/templates/vue/package.json +35 -0
- package/templates/vue/pnpm-workspace.yaml +4 -0
- package/templates/vue/server/auth/backend.mjs +83 -0
- package/templates/vue/server/auth/client-ip.mjs +52 -0
- package/templates/vue/server/auth/oauth.mjs +213 -0
- package/templates/vue/server/auth/routes.mjs +254 -0
- package/templates/vue/server/auth/session.mjs +180 -0
- package/templates/vue/server/client-manifest.mjs +116 -0
- package/templates/vue/server/email.mjs +36 -0
- package/templates/vue/server/inertia.mjs +79 -0
- package/templates/vue/server/render-token.mjs +81 -0
- package/templates/vue/server/tester.mjs +228 -0
- package/templates/vue/server.mjs +526 -0
- package/templates/vue/ssr-renderer.ts +146 -0
- package/templates/vue/tsconfig.json +31 -0
- package/templates/vue/typecheck-loader.mjs +13 -0
- package/templates/vue/vite.config.ts +161 -0
- package/templates/vue/vite.email.config.ts +77 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { type Component, computed } from "vue";
|
|
3
|
+
import { resolveDmsComponent, useDmsRoute } from "./frontend-module";
|
|
4
|
+
|
|
5
|
+
interface ComponentChild {
|
|
6
|
+
id: string;
|
|
7
|
+
component: ComponentDefinition;
|
|
8
|
+
slot?: string;
|
|
9
|
+
colSpan?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ComponentDefinition {
|
|
13
|
+
componentName?: string;
|
|
14
|
+
options?: Record<string, unknown>;
|
|
15
|
+
children?: ComponentChild[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ResolvedComponentDefinition {
|
|
19
|
+
id: string;
|
|
20
|
+
component: Component | string;
|
|
21
|
+
componentName?: string;
|
|
22
|
+
options?: Record<string, unknown>;
|
|
23
|
+
children: ResolvedComponentDefinition[];
|
|
24
|
+
slot?: string;
|
|
25
|
+
colSpan?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface PageLayout {
|
|
29
|
+
components?: Record<string, ComponentDefinition>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface PageRoute {
|
|
33
|
+
fullId?: string;
|
|
34
|
+
fullSlug?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface PagePayload {
|
|
38
|
+
layout?: PageLayout;
|
|
39
|
+
route?: PageRoute;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface DynamicPageProps {
|
|
43
|
+
page: PagePayload;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const props = defineProps<DynamicPageProps>();
|
|
47
|
+
// biome-ignore lint/correctness/noUnusedVariables: Referenced by the Vue template.
|
|
48
|
+
const route = useDmsRoute(props.page.route?.fullSlug);
|
|
49
|
+
|
|
50
|
+
function resolveDefinition(
|
|
51
|
+
id: string,
|
|
52
|
+
definition: ComponentDefinition,
|
|
53
|
+
placement: Omit<ComponentChild, "id" | "component"> = {},
|
|
54
|
+
): ResolvedComponentDefinition {
|
|
55
|
+
return {
|
|
56
|
+
id,
|
|
57
|
+
component: definition.componentName
|
|
58
|
+
? resolveDmsComponent(definition.componentName) || "div"
|
|
59
|
+
: "div",
|
|
60
|
+
componentName: definition.componentName,
|
|
61
|
+
options: definition.options,
|
|
62
|
+
children: (definition.children ?? []).map((child) => {
|
|
63
|
+
const { id: childId, component, ...childPlacement } = child;
|
|
64
|
+
return resolveDefinition(childId, component, childPlacement);
|
|
65
|
+
}),
|
|
66
|
+
...placement,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// biome-ignore lint/correctness/noUnusedVariables: Referenced by the Vue template.
|
|
71
|
+
const components = computed(() =>
|
|
72
|
+
Object.entries(props.page.layout?.components ?? {}).map(([id, definition]) =>
|
|
73
|
+
resolveDefinition(id, definition),
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<template>
|
|
79
|
+
<div class="dms-page-stack space-y-6">
|
|
80
|
+
<div v-for="component in components" :key="component.id">
|
|
81
|
+
<DmsRecursiveComponent
|
|
82
|
+
:component="component"
|
|
83
|
+
:page-id="page.route?.fullId ?? ''"
|
|
84
|
+
:component-id="component.id"
|
|
85
|
+
:route-params="route.params"
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default {};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import UApp from "@nuxt/ui/components/App.vue";
|
|
2
|
+
import UIcon from "@nuxt/ui/runtime/vue/components/Icon.vue";
|
|
3
|
+
import { useAppConfig } from "@nuxt/ui/runtime/vue/composables/useAppConfig";
|
|
4
|
+
import ui from "@nuxt/ui/vue-plugin";
|
|
5
|
+
import { useHead } from "@unhead/vue";
|
|
6
|
+
import type { VueHeadClient } from "@unhead/vue/types";
|
|
7
|
+
import { defu } from "defu";
|
|
8
|
+
import type { FetchOptions } from "ofetch";
|
|
9
|
+
import {
|
|
10
|
+
type App,
|
|
11
|
+
type Component,
|
|
12
|
+
computed,
|
|
13
|
+
defineComponent,
|
|
14
|
+
h,
|
|
15
|
+
type Plugin,
|
|
16
|
+
Suspense,
|
|
17
|
+
type VNode,
|
|
18
|
+
watch,
|
|
19
|
+
} from "vue";
|
|
20
|
+
import { createI18n, useI18n } from "vue-i18n";
|
|
21
|
+
import DmsDynamicPage from "./DmsDynamicPage.vue";
|
|
22
|
+
import {
|
|
23
|
+
type DmsLocaleLoader,
|
|
24
|
+
type DmsPageProps,
|
|
25
|
+
createDmsFrontendRuntime,
|
|
26
|
+
getDmsErrorPage,
|
|
27
|
+
getDmsLayout,
|
|
28
|
+
getDmsLayoutProps,
|
|
29
|
+
getDmsPage,
|
|
30
|
+
hydrateDmsPageProps,
|
|
31
|
+
installDmsPlugins,
|
|
32
|
+
preloadDmsPage,
|
|
33
|
+
provideDmsFrontendRuntime,
|
|
34
|
+
resolveDmsComponent,
|
|
35
|
+
serializeDmsAsyncData,
|
|
36
|
+
useDmsAppConfig,
|
|
37
|
+
useDmsState,
|
|
38
|
+
useError,
|
|
39
|
+
} from "./frontend-module";
|
|
40
|
+
import {
|
|
41
|
+
loadLocaleMessages,
|
|
42
|
+
localeMessages,
|
|
43
|
+
supportedLocales,
|
|
44
|
+
} from "./locales.generated";
|
|
45
|
+
|
|
46
|
+
export interface DmsInertiaSetupProps {
|
|
47
|
+
initialPage: unknown;
|
|
48
|
+
initialComponent: Component;
|
|
49
|
+
resolveComponent: (name: string) => Promise<Component>;
|
|
50
|
+
titleCallback?: (title: string) => string;
|
|
51
|
+
onHeadUpdate?: (elements: string[]) => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface DmsAppOptions {
|
|
55
|
+
app: App;
|
|
56
|
+
head: VueHeadClient;
|
|
57
|
+
initialPageProps: DmsPageProps;
|
|
58
|
+
initialPageUrl: string;
|
|
59
|
+
inertiaPlugin: Plugin;
|
|
60
|
+
runtime?: DmsFrontendRuntime;
|
|
61
|
+
serverFetch?: DmsServerFetch;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface DmsConfiguredApp {
|
|
65
|
+
mounted: () => Promise<void>;
|
|
66
|
+
runtime: ReturnType<typeof createDmsFrontendRuntime>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type DmsServerFetch = (
|
|
70
|
+
request: string,
|
|
71
|
+
options?: FetchOptions,
|
|
72
|
+
) => Promise<unknown>;
|
|
73
|
+
|
|
74
|
+
const SSR_ASYNC_DATA_ID = "dms-ssr-async-data";
|
|
75
|
+
const DEFAULT_LOCALE = "en";
|
|
76
|
+
|
|
77
|
+
function pageLocale(props: DmsPageProps): string {
|
|
78
|
+
const language = props.user?.language;
|
|
79
|
+
if (typeof language !== "string") return DEFAULT_LOCALE;
|
|
80
|
+
const locale = language.slice(0, 2);
|
|
81
|
+
return supportedLocales.includes(locale) ? locale : DEFAULT_LOCALE;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readDmsAsyncData(): Record<string, unknown> {
|
|
85
|
+
if (typeof document === "undefined") return {};
|
|
86
|
+
const element = document.getElementById(SSR_ASYNC_DATA_ID);
|
|
87
|
+
if (!element?.textContent) return {};
|
|
88
|
+
const data = JSON.parse(element.textContent) as Record<string, unknown>;
|
|
89
|
+
element.remove();
|
|
90
|
+
return data;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const DmsInertiaPage = defineComponent({
|
|
94
|
+
name: "DmsInertiaPage",
|
|
95
|
+
inheritAttrs: false,
|
|
96
|
+
setup(_, { attrs }) {
|
|
97
|
+
const props = computed(() => attrs as unknown as DmsPageProps);
|
|
98
|
+
watch(
|
|
99
|
+
() => [attrs.path, attrs.page, attrs.error],
|
|
100
|
+
() => {
|
|
101
|
+
void preloadDmsPage(props.value);
|
|
102
|
+
},
|
|
103
|
+
{ immediate: true },
|
|
104
|
+
);
|
|
105
|
+
const { t } = useI18n();
|
|
106
|
+
useHead({
|
|
107
|
+
title: computed(() => {
|
|
108
|
+
const title = props.value.page.route?.displayName;
|
|
109
|
+
return title?.startsWith("$") ? t(title.slice(1)) : title;
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
return () => renderDmsPage(props.value);
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const DmsPersistentLayout = defineComponent({
|
|
117
|
+
name: "DmsPersistentLayout",
|
|
118
|
+
inheritAttrs: false,
|
|
119
|
+
setup(_, { attrs, slots }) {
|
|
120
|
+
const props = computed(() => attrs as unknown as DmsPageProps);
|
|
121
|
+
const i18n = useI18n();
|
|
122
|
+
let localeVersion = 0;
|
|
123
|
+
useHead({
|
|
124
|
+
htmlAttrs: { lang: computed(() => i18n.locale.value) },
|
|
125
|
+
});
|
|
126
|
+
watch(
|
|
127
|
+
() => [attrs.path, attrs.page, attrs.user, attrs.session, attrs.error],
|
|
128
|
+
async () => {
|
|
129
|
+
const version = ++localeVersion;
|
|
130
|
+
hydrateDmsPageProps(props.value);
|
|
131
|
+
const locale = pageLocale(props.value);
|
|
132
|
+
if (i18n.locale.value === locale) return;
|
|
133
|
+
const messages = await loadLocaleMessages(locale);
|
|
134
|
+
if (version !== localeVersion) return;
|
|
135
|
+
i18n.setLocaleMessage(locale, messages);
|
|
136
|
+
i18n.locale.value = locale;
|
|
137
|
+
},
|
|
138
|
+
{ immediate: true, flush: "sync" },
|
|
139
|
+
);
|
|
140
|
+
const currentError = useError();
|
|
141
|
+
const overlays = useDmsState<string[]>("dms-app-overlays", () => []);
|
|
142
|
+
return () => {
|
|
143
|
+
const error = currentError.value ?? props.value.error;
|
|
144
|
+
const content = error
|
|
145
|
+
? [renderDmsPage(props.value, error)]
|
|
146
|
+
: slots.default?.();
|
|
147
|
+
return renderDmsPersistentLayout(
|
|
148
|
+
props.value,
|
|
149
|
+
error,
|
|
150
|
+
content,
|
|
151
|
+
overlays.value,
|
|
152
|
+
);
|
|
153
|
+
};
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const DmsPersistentInertiaPage = Object.assign(DmsInertiaPage, {
|
|
158
|
+
layout: DmsPersistentLayout,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
export function resolveDmsInertiaPage(): Component {
|
|
162
|
+
return DmsPersistentInertiaPage;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function renderDmsPage(
|
|
166
|
+
props: DmsPageProps,
|
|
167
|
+
capturedError: DmsPageProps["error"] = useError().value,
|
|
168
|
+
) {
|
|
169
|
+
const error = capturedError ?? props.error;
|
|
170
|
+
const page = error
|
|
171
|
+
? (getDmsErrorPage() ?? DmsDynamicPage)
|
|
172
|
+
: (getDmsPage(props) ?? DmsDynamicPage);
|
|
173
|
+
const pageContent = h(page as Component, {
|
|
174
|
+
...props,
|
|
175
|
+
error,
|
|
176
|
+
key: props.path,
|
|
177
|
+
});
|
|
178
|
+
return h(Suspense, null, { default: () => pageContent });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function renderDmsPersistentLayout(
|
|
182
|
+
props: DmsPageProps,
|
|
183
|
+
error: DmsPageProps["error"],
|
|
184
|
+
children: VNode[] | undefined,
|
|
185
|
+
overlays: string[],
|
|
186
|
+
) {
|
|
187
|
+
const layout = error ? undefined : getDmsLayout(props);
|
|
188
|
+
const content = layout
|
|
189
|
+
? h(layout, getDmsLayoutProps(props), { default: () => children })
|
|
190
|
+
: children;
|
|
191
|
+
return h(
|
|
192
|
+
UApp,
|
|
193
|
+
{ portal: "#dms-overlays" },
|
|
194
|
+
{
|
|
195
|
+
default: () => [
|
|
196
|
+
...overlays.map((name) =>
|
|
197
|
+
h(resolveDmsComponent(name) ?? name, { key: name }),
|
|
198
|
+
),
|
|
199
|
+
content,
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function configureDmsApp(
|
|
206
|
+
options: DmsAppOptions,
|
|
207
|
+
): Promise<DmsConfiguredApp> {
|
|
208
|
+
const uiAppConfig = useAppConfig();
|
|
209
|
+
Object.assign(uiAppConfig, defu(useDmsAppConfig(), uiAppConfig));
|
|
210
|
+
const runtime =
|
|
211
|
+
options.runtime ??
|
|
212
|
+
createDmsFrontendRuntime(
|
|
213
|
+
options.serverFetch as typeof import("ofetch").ofetch,
|
|
214
|
+
readDmsAsyncData(),
|
|
215
|
+
);
|
|
216
|
+
const locale = pageLocale(options.initialPageProps);
|
|
217
|
+
const messages = await loadLocaleMessages(locale);
|
|
218
|
+
const i18n = createI18n({
|
|
219
|
+
legacy: false,
|
|
220
|
+
locale,
|
|
221
|
+
fallbackLocale: DEFAULT_LOCALE,
|
|
222
|
+
messages: { ...localeMessages, [locale]: messages },
|
|
223
|
+
});
|
|
224
|
+
provideDmsFrontendRuntime(options.app, runtime);
|
|
225
|
+
options.app.component("Icon", UIcon);
|
|
226
|
+
options.app.use(options.inertiaPlugin).use(options.head).use(ui).use(i18n);
|
|
227
|
+
options.app.runWithContext(() =>
|
|
228
|
+
hydrateDmsPageProps(options.initialPageProps, options.initialPageUrl),
|
|
229
|
+
);
|
|
230
|
+
const mounted = await installDmsPlugins(
|
|
231
|
+
options.app,
|
|
232
|
+
i18n.global,
|
|
233
|
+
runtime,
|
|
234
|
+
loadLocaleMessages as DmsLocaleLoader,
|
|
235
|
+
supportedLocales,
|
|
236
|
+
);
|
|
237
|
+
return { mounted, runtime };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export { serializeDmsAsyncData };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { extname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { brotliCompress, constants, gzip } from "node:zlib";
|
|
6
|
+
|
|
7
|
+
const ASSET_ROOT = fileURLToPath(new URL("./dist/client/", import.meta.url));
|
|
8
|
+
const COMPRESSIBLE_EXTENSIONS = new Set([
|
|
9
|
+
".css",
|
|
10
|
+
".html",
|
|
11
|
+
".js",
|
|
12
|
+
".json",
|
|
13
|
+
".svg",
|
|
14
|
+
]);
|
|
15
|
+
const MINIMUM_SIZE_BYTES = 1_024;
|
|
16
|
+
const compressBrotli = promisify(brotliCompress);
|
|
17
|
+
const compressGzip = promisify(gzip);
|
|
18
|
+
|
|
19
|
+
async function assetFiles(directory) {
|
|
20
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
21
|
+
const files = await Promise.all(
|
|
22
|
+
entries.map((entry) => {
|
|
23
|
+
const path = join(directory, entry.name);
|
|
24
|
+
return entry.isDirectory() ? assetFiles(path) : [path];
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
return files.flat();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function compressAsset(path) {
|
|
31
|
+
if (!COMPRESSIBLE_EXTENSIONS.has(extname(path))) return;
|
|
32
|
+
const content = await readFile(path);
|
|
33
|
+
if (content.length < MINIMUM_SIZE_BYTES) return;
|
|
34
|
+
const [brotli, compressedGzip] = await Promise.all([
|
|
35
|
+
compressBrotli(content, {
|
|
36
|
+
params: {
|
|
37
|
+
[constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY,
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
compressGzip(content, { level: constants.Z_BEST_COMPRESSION }),
|
|
41
|
+
]);
|
|
42
|
+
await Promise.all([
|
|
43
|
+
writeFile(`${path}.br`, brotli),
|
|
44
|
+
writeFile(`${path}.gz`, compressedGzip),
|
|
45
|
+
]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const path of await assetFiles(ASSET_ROOT)) await compressAsset(path);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import supportedLocales from "./email-locales.generated.json";
|
|
3
|
+
|
|
4
|
+
type Messages = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
// Catalogs are immutable build data, shared across renders. Each Vue i18n
|
|
7
|
+
// instance receives only its requested language and the English fallback.
|
|
8
|
+
const catalogs = new Map<string, Promise<Messages>>();
|
|
9
|
+
|
|
10
|
+
function readCatalog(locale: string): Promise<Messages> {
|
|
11
|
+
let catalog = catalogs.get(locale);
|
|
12
|
+
if (!catalog) {
|
|
13
|
+
catalog = readFile(
|
|
14
|
+
new URL(`./locales/${locale}.json`, import.meta.url),
|
|
15
|
+
"utf8",
|
|
16
|
+
).then((content) => JSON.parse(content) as Messages);
|
|
17
|
+
catalogs.set(locale, catalog);
|
|
18
|
+
}
|
|
19
|
+
return catalog;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function loadEmailLocaleMessages(
|
|
23
|
+
locale: string,
|
|
24
|
+
): Promise<Record<string, Messages>> {
|
|
25
|
+
const fallback = await readCatalog("en");
|
|
26
|
+
const normalized = locale.slice(0, 2);
|
|
27
|
+
const selected =
|
|
28
|
+
supportedLocales.includes(normalized) && normalized !== "en"
|
|
29
|
+
? await readCatalog(normalized)
|
|
30
|
+
: fallback;
|
|
31
|
+
return { en: fallback, [locale]: selected };
|
|
32
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { renderToString } from "@vue/server-renderer";
|
|
2
|
+
import { defu } from "defu";
|
|
3
|
+
import { type Component, createSSRApp, h } from "vue";
|
|
4
|
+
import { createI18n } from "vue-i18n";
|
|
5
|
+
import moduleRegistry from "./generated-frontend-modules.json";
|
|
6
|
+
import { loadEmailLocaleMessages } from "./email-locales";
|
|
7
|
+
|
|
8
|
+
interface EmailModule {
|
|
9
|
+
default: Component;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface EmailRegistry {
|
|
13
|
+
serverEmailTemplates: Record<string, () => Promise<EmailModule>>;
|
|
14
|
+
appConfig?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface EmailRenderOptions {
|
|
18
|
+
locale?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const registries = import.meta.glob<EmailRegistry>(
|
|
22
|
+
"/frontend-modules/*/dms.email.ts",
|
|
23
|
+
{ eager: true },
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const orderedRegistries = moduleRegistry.modules
|
|
27
|
+
.map((module) => registries[`/frontend-modules/${module.id}/dms.email.ts`])
|
|
28
|
+
.filter((registry): registry is EmailRegistry => Boolean(registry));
|
|
29
|
+
|
|
30
|
+
function templateEntries(): Array<[string, () => Promise<EmailModule>]> {
|
|
31
|
+
return orderedRegistries.flatMap((registry) =>
|
|
32
|
+
Object.entries(registry.serverEmailTemplates),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const EMAIL_ELEMENTS: Record<string, string> = {
|
|
37
|
+
EBody: "body",
|
|
38
|
+
EButton: "a",
|
|
39
|
+
EHr: "hr",
|
|
40
|
+
EHtml: "html",
|
|
41
|
+
EImg: "img",
|
|
42
|
+
EText: "p",
|
|
43
|
+
ELink: "a",
|
|
44
|
+
EColumn: "td",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function registerEmailElements(app: ReturnType<typeof createSSRApp>): void {
|
|
48
|
+
Object.entries(EMAIL_ELEMENTS).forEach(([name, tag]) => {
|
|
49
|
+
app.component(name, (_props, context) =>
|
|
50
|
+
h(tag, context.attrs, context.slots.default?.()),
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
for (const name of ["EContainer", "ESection"]) {
|
|
54
|
+
app.component(name, (_props, { attrs, slots }) =>
|
|
55
|
+
h(
|
|
56
|
+
"table",
|
|
57
|
+
{
|
|
58
|
+
align: "center",
|
|
59
|
+
width: "100%",
|
|
60
|
+
role: "presentation",
|
|
61
|
+
cellSpacing: 0,
|
|
62
|
+
cellPadding: 0,
|
|
63
|
+
border: 0,
|
|
64
|
+
...attrs,
|
|
65
|
+
style: [
|
|
66
|
+
name === "EContainer" ? { maxWidth: "37.5em" } : {},
|
|
67
|
+
attrs.style,
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
[h("tbody", null, [h("tr", null, [h("td", null, slots.default?.())])])],
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
app.component("EHead", (_props, { attrs, slots }) =>
|
|
75
|
+
h("head", attrs, [h("meta", { charset: "utf-8" }), slots.default?.()]),
|
|
76
|
+
);
|
|
77
|
+
app.component("EHeading", (_props, { attrs, slots }) => {
|
|
78
|
+
const { as, ...attributes } = attrs;
|
|
79
|
+
const tag = typeof as === "string" && /^h[1-6]$/.test(as) ? as : "h1";
|
|
80
|
+
return h(tag, attributes, slots.default?.());
|
|
81
|
+
});
|
|
82
|
+
app.component("ERow", (_props, { attrs, slots }) =>
|
|
83
|
+
h(
|
|
84
|
+
"table",
|
|
85
|
+
{
|
|
86
|
+
role: "presentation",
|
|
87
|
+
width: "100%",
|
|
88
|
+
cellPadding: 0,
|
|
89
|
+
cellSpacing: 0,
|
|
90
|
+
border: 0,
|
|
91
|
+
...attrs,
|
|
92
|
+
},
|
|
93
|
+
[h("tbody", null, [h("tr", null, slots.default?.())])],
|
|
94
|
+
),
|
|
95
|
+
);
|
|
96
|
+
app.component("EPreview", (_props, { attrs, slots }) =>
|
|
97
|
+
h(
|
|
98
|
+
"div",
|
|
99
|
+
{
|
|
100
|
+
...attrs,
|
|
101
|
+
style: [
|
|
102
|
+
attrs.style,
|
|
103
|
+
{
|
|
104
|
+
display: "none",
|
|
105
|
+
overflow: "hidden",
|
|
106
|
+
lineHeight: "1px",
|
|
107
|
+
opacity: 0,
|
|
108
|
+
maxHeight: 0,
|
|
109
|
+
maxWidth: 0,
|
|
110
|
+
msoHide: "all",
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
slots.default?.(),
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function renderEmail(
|
|
120
|
+
templateName: string,
|
|
121
|
+
props: Record<string, unknown>,
|
|
122
|
+
options: EmailRenderOptions = {},
|
|
123
|
+
): Promise<string> {
|
|
124
|
+
const suffix = `/${templateName}.vue`;
|
|
125
|
+
const entry = templateEntries().find(([path]) => path.endsWith(suffix));
|
|
126
|
+
if (!entry) throw new Error(`Unknown email template: ${templateName}`);
|
|
127
|
+
const component = (await entry[1]()).default;
|
|
128
|
+
const app = createSSRApp(component, props);
|
|
129
|
+
const locale = options.locale ?? "en";
|
|
130
|
+
const messages = await loadEmailLocaleMessages(locale);
|
|
131
|
+
app.use(
|
|
132
|
+
createI18n({
|
|
133
|
+
legacy: false,
|
|
134
|
+
locale,
|
|
135
|
+
fallbackLocale: "en",
|
|
136
|
+
messages,
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
app.provide("dmsEmailLocale", locale);
|
|
140
|
+
app.provide(
|
|
141
|
+
"dmsEmailAppConfig",
|
|
142
|
+
defu({}, ...orderedRegistries.map((registry) => registry.appConfig ?? {})),
|
|
143
|
+
);
|
|
144
|
+
const publicConfig = defu(
|
|
145
|
+
{},
|
|
146
|
+
...moduleRegistry.modules.map((module) => module.options),
|
|
147
|
+
);
|
|
148
|
+
app.provide("dmsEmailRuntimeConfig", {
|
|
149
|
+
public: defu(
|
|
150
|
+
process.env.DMS_CLIENT_URL
|
|
151
|
+
? { dms: { clientBaseUrl: process.env.DMS_CLIENT_URL } }
|
|
152
|
+
: {},
|
|
153
|
+
publicConfig,
|
|
154
|
+
{ dms: { clientBaseUrl: "" } },
|
|
155
|
+
),
|
|
156
|
+
});
|
|
157
|
+
registerEmailElements(app);
|
|
158
|
+
return `<!doctype html>${await renderToString(app)}`;
|
|
159
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { inject } from "vue";
|
|
2
|
+
|
|
3
|
+
export interface EmailRuntimeConfig {
|
|
4
|
+
public: Record<string, unknown> & {
|
|
5
|
+
dms: { clientBaseUrl: string };
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function useDmsAppConfig(): Record<string, unknown> {
|
|
10
|
+
return inject<Record<string, unknown>>("dmsEmailAppConfig", {});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useDmsRuntimeConfig(): EmailRuntimeConfig {
|
|
14
|
+
return inject<EmailRuntimeConfig>("dmsEmailRuntimeConfig", {
|
|
15
|
+
public: { dms: { clientBaseUrl: process.env.DMS_CLIENT_URL ?? "" } },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function defineAppConfig<T extends Record<string, unknown>>(
|
|
20
|
+
config: T,
|
|
21
|
+
): T {
|
|
22
|
+
return config;
|
|
23
|
+
}
|