@myelophone/nuxt 0.18.2
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/.devcontainer/Dockerfile +7 -0
- package/.devcontainer/devcontainer.json +21 -0
- package/.editorconfig +43 -0
- package/.github/workflows/package.yml +73 -0
- package/.nuxtignore +6 -0
- package/.releaserc.js +132 -0
- package/.yarn/releases/yarn-4.18.0.cjs +1000 -0
- package/.yarnrc.yml +8 -0
- package/CHANGELOG.md +141 -0
- package/LICENSE +77 -0
- package/README.md +3004 -0
- package/app/app.vue +455 -0
- package/app/assets/css/animation.css +81 -0
- package/app/assets/css/backgrounds.css +57 -0
- package/app/assets/css/base.css +182 -0
- package/app/assets/css/index.css +9 -0
- package/app/assets/css/links.css +12 -0
- package/app/assets/css/multimedia.css +102 -0
- package/app/assets/css/preloader.css +68 -0
- package/app/assets/css/print.css +40 -0
- package/app/assets/css/tailwind.css +20 -0
- package/app/assets/css/theme.css +29 -0
- package/app/components/consent/GoogleMap.vue +59 -0
- package/app/components/consent/Youtube.vue +27 -0
- package/app/components/cookie/Banner.vue +70 -0
- package/app/components/cookie/ConsentWrapper.vue +71 -0
- package/app/components/cookie/PrivacyPolicy.vue +179 -0
- package/app/components/cookie/SettingsModal.vue +387 -0
- package/app/components/grid/Col.vue +262 -0
- package/app/components/grid/Container.vue +236 -0
- package/app/components/grid/Row.vue +248 -0
- package/app/components/grid/Stack.vue +233 -0
- package/app/components/myelophone/Copyright.vue +5 -0
- package/app/components/myelophone/Welcome.vue +107 -0
- package/app/components/page/FullscreenPreloader.vue +177 -0
- package/app/components/page/Preloader.vue +18 -0
- package/app/components/page/ReadyLocalized.vue +19 -0
- package/app/components/ready/.gitkeep +0 -0
- package/app/components/safe/Email.vue +52 -0
- package/app/components/safe/ImgWithLoader.vue +180 -0
- package/app/components/safe/NuxtImg.vue +25 -0
- package/app/components/safe/NuxtPicture.vue +29 -0
- package/app/components/section/.gitkeep +0 -0
- package/app/components/seo/BrandInfo.vue +76 -0
- package/app/components/seo/ContentNoIndex.vue +35 -0
- package/app/components/seo/NoIndex.vue +22 -0
- package/app/components/seo/OrgInfo.vue +130 -0
- package/app/components/seo/ProfileInfo.vue +137 -0
- package/app/components/ui/Accordion.vue +163 -0
- package/app/components/ui/Alert.vue +179 -0
- package/app/components/ui/Avatar.vue +88 -0
- package/app/components/ui/Badge.vue +150 -0
- package/app/components/ui/Banner.vue +133 -0
- package/app/components/ui/Button.vue +175 -0
- package/app/components/ui/Card.vue +66 -0
- package/app/components/ui/Checkbox.vue +134 -0
- package/app/components/ui/Chip.vue +136 -0
- package/app/components/ui/CommandPalette.vue +434 -0
- package/app/components/ui/Console.vue +583 -0
- package/app/components/ui/CursorCreative.vue +148 -0
- package/app/components/ui/FeatureAccordion.vue +165 -0
- package/app/components/ui/FileUpload.vue +705 -0
- package/app/components/ui/GeoDependent.vue +72 -0
- package/app/components/ui/Heading.vue +51 -0
- package/app/components/ui/Icon.vue +42 -0
- package/app/components/ui/Icon8.vue +39 -0
- package/app/components/ui/Input.vue +88 -0
- package/app/components/ui/LangLink.vue +34 -0
- package/app/components/ui/LangVisible.vue +33 -0
- package/app/components/ui/LanguageSelect.vue +202 -0
- package/app/components/ui/LightBox.vue +155 -0
- package/app/components/ui/LocalTime.vue +170 -0
- package/app/components/ui/Modal.vue +161 -0
- package/app/components/ui/Popup.vue +200 -0
- package/app/components/ui/PostContent.server.vue +23 -0
- package/app/components/ui/ProtectedEmail.vue +52 -0
- package/app/components/ui/RelativeTime.vue +141 -0
- package/app/components/ui/ScheduledContent.vue +150 -0
- package/app/components/ui/ScrollToTop.client.vue +48 -0
- package/app/components/ui/SegmentedControl.vue +148 -0
- package/app/components/ui/SiteSearch.vue +1191 -0
- package/app/components/ui/Skeleton.vue +91 -0
- package/app/components/ui/SmartContrast.vue +74 -0
- package/app/components/ui/SnapContainer.vue +222 -0
- package/app/components/ui/SnapSection.vue +294 -0
- package/app/components/ui/SonnerStyle.vue +7 -0
- package/app/components/ui/SplitSection.vue +99 -0
- package/app/components/ui/Steps.vue +858 -0
- package/app/components/ui/StickyWrapper.vue +46 -0
- package/app/components/ui/Table.vue +251 -0
- package/app/components/ui/Tabs.vue +451 -0
- package/app/components/ui/TextColumn.vue +36 -0
- package/app/components/ui/Textarea.vue +251 -0
- package/app/components/ui/Toggler.vue +40 -0
- package/app/components/ui/Tooltip.vue +524 -0
- package/app/components/ui/TruncateText.vue +105 -0
- package/app/components/ui/ViewportSpacer.vue +72 -0
- package/app/components/view/AnimatedCounter.vue +121 -0
- package/app/components/view/AnnouncementBar.vue +104 -0
- package/app/components/view/AvatarGroup.vue +104 -0
- package/app/components/view/Breadcrumbs.vue +97 -0
- package/app/components/view/ContentWithToc.vue +152 -0
- package/app/components/view/Copyright.vue +68 -0
- package/app/components/view/CountdownTimer.vue +184 -0
- package/app/components/view/CreativeCTA.vue +143 -0
- package/app/components/view/CurrencySelect.vue +254 -0
- package/app/components/view/FeatureBoxGrid.vue +142 -0
- package/app/components/view/HorizontalMenu.vue +97 -0
- package/app/components/view/ImageCompare.vue +141 -0
- package/app/components/view/ImgSlider.vue +333 -0
- package/app/components/view/InfiniteMarquee.vue +147 -0
- package/app/components/view/InfoBar.vue +86 -0
- package/app/components/view/LogoGrid.vue +106 -0
- package/app/components/view/LogoSlider.vue +83 -0
- package/app/components/view/NotFound.vue +81 -0
- package/app/components/view/PartialBackground.vue +313 -0
- package/app/components/view/QuoteBig.vue +111 -0
- package/app/components/view/ResponsiveMenu.vue +219 -0
- package/app/components/view/ReviewsSlider.vue +230 -0
- package/app/components/view/SocialBar.vue +78 -0
- package/app/components/view/StickyHeadingContent.vue +158 -0
- package/app/components/view/TallyForm.vue +295 -0
- package/app/components/view/WarningOutdated.vue +113 -0
- package/app/composables/useApi.ts +88 -0
- package/app/composables/useAppSeo.ts +78 -0
- package/app/composables/useBreadcrumbs.ts +106 -0
- package/app/composables/useChangeUrl.ts +73 -0
- package/app/composables/useCommand.ts +28 -0
- package/app/composables/useCookieControl.ts +111 -0
- package/app/composables/useCookieModal.ts +9 -0
- package/app/composables/useDevice.ts +109 -0
- package/app/composables/useGeoFetch.ts +139 -0
- package/app/composables/useLoadScript.ts +36 -0
- package/app/composables/useModalState.ts +37 -0
- package/app/composables/useMultiLang.ts +278 -0
- package/app/composables/useNetwork.ts +49 -0
- package/app/composables/useNuxtVersion.ts +5 -0
- package/app/composables/usePiniaStore.ts +5 -0
- package/app/composables/usePopupControl.ts +57 -0
- package/app/composables/useQuery.ts +307 -0
- package/app/composables/useReadyPage.ts +182 -0
- package/app/composables/useSafeList.ts +90 -0
- package/app/composables/useScriptLoader.ts +221 -0
- package/app/composables/useSiteSearch.ts +32 -0
- package/app/composables/useSnapScroll.ts +24 -0
- package/app/composables/useStatic.ts +5 -0
- package/app/composables/useStorage.ts +252 -0
- package/app/composables/useStoreBroadcast.ts +146 -0
- package/app/composables/useThemeSync.ts +81 -0
- package/app/constants/layout.ts +109 -0
- package/app/constants/predefinedCookieScripts.ts +1222 -0
- package/app/constants/ui-events.ts +1 -0
- package/app/error.vue +169 -0
- package/app/layouts/default-blog.vue +5 -0
- package/app/layouts/default.vue +3 -0
- package/app/locales/en/commands.json +5 -0
- package/app/locales/en/cookies.json +182 -0
- package/app/locales/en/error.json +4 -0
- package/app/locales/en/interface.json +33 -0
- package/app/locales/en/myelophone.json +8 -0
- package/app/locales/en/outdated.json +5 -0
- package/app/locales/en/times.json +18 -0
- package/app/locales/pl/commands.json +5 -0
- package/app/locales/pl/cookies.json +182 -0
- package/app/locales/pl/error.json +4 -0
- package/app/locales/pl/interface.json +33 -0
- package/app/locales/pl/myelophone.json +8 -0
- package/app/locales/pl/outdated.json +5 -0
- package/app/locales/pl/times.json +18 -0
- package/app/locales/ru/commands.json +5 -0
- package/app/locales/ru/cookies.json +182 -0
- package/app/locales/ru/error.json +4 -0
- package/app/locales/ru/interface.json +33 -0
- package/app/locales/ru/myelophone.json +8 -0
- package/app/locales/ru/outdated.json +5 -0
- package/app/locales/ru/times.json +18 -0
- package/app/middleware/blog-layout.ts +28 -0
- package/app/middleware/i18n.global.ts +17 -0
- package/app/middleware/not-found-status.ts +11 -0
- package/app/modules/default-blog/index.ts +39 -0
- package/app/modules/default-blog/posts.ts +80 -0
- package/app/modules/default-sitemap/index.ts +116 -0
- package/app/modules/default-sitemap/runtime/sitemap.get.ts +42 -0
- package/app/modules/fullscreen-preloader/module.ts +43 -0
- package/app/modules/htaccess/.htaccess +38 -0
- package/app/modules/htaccess/index.ts +23 -0
- package/app/modules/i18n-shaker.mjs +221 -0
- package/app/modules/multi18n/module.ts +71 -0
- package/app/modules/myelophone-config/module.ts +241 -0
- package/app/pages/[...all].vue +9 -0
- package/app/pages/index.vue +3 -0
- package/app/pages/post/[...slug].vue +30 -0
- package/app/plugins/bfcache-refresh.client.ts +9 -0
- package/app/plugins/canonical.ts +30 -0
- package/app/plugins/csp-nonce.server.ts +41 -0
- package/app/plugins/multi18n.client.ts +21 -0
- package/app/plugins/multi18n.server.ts +15 -0
- package/app/plugins/nuxt-version.client.ts +4 -0
- package/app/plugins/restore.client.ts +21 -0
- package/app/plugins/reveal.ts +89 -0
- package/app/plugins/url-helper.client.ts +66 -0
- package/app/server/api/exchange-rates.get.ts +216 -0
- package/app/server/api/healthz.ts +1 -0
- package/app/server/api/logout.post.ts +6 -0
- package/app/server/middleware/normalize-url.ts +14 -0
- package/app/server/middleware/security-headers.ts +147 -0
- package/app/server/middleware/url-helper.ts +43 -0
- package/app/server/plugins/clean-html-spaces.ts +15 -0
- package/app/server/plugins/clean-styles.ts +94 -0
- package/app/server/plugins/compression.ts +122 -0
- package/app/server/plugins/css-preload.ts +26 -0
- package/app/server/plugins/early-hints.ts +31 -0
- package/app/server/plugins/force-noindex.ts +43 -0
- package/app/server/plugins/html-one-line.ts +17 -0
- package/app/server/plugins/inline-style.ts +7 -0
- package/app/server/routes/healthz.get.ts +1 -0
- package/app/server/tsconfig.json +3 -0
- package/app/stores/cart.ts +945 -0
- package/app/stores/commands.ts +43 -0
- package/app/stores/device.ts +156 -0
- package/app/stores/querySync.ts +5 -0
- package/app/stores/settings.ts +145 -0
- package/app/stores/user.ts +607 -0
- package/app/types/cookie.ts +89 -0
- package/app/types/myelophone.d.ts +87 -0
- package/app/types/site-search.ts +110 -0
- package/app/utils/asyncRefresh.ts +4 -0
- package/app/utils/blog.server.ts +142 -0
- package/app/utils/jsonLd.ts +56 -0
- package/app/utils/myelophoneConfig.ts +4 -0
- package/app/utils/sortBy.ts +18 -0
- package/app/utils/transliterate.ts +75 -0
- package/app/utils/ytVideo.ts +37 -0
- package/app/workers/site-search.worker.ts +661 -0
- package/biome.json +94 -0
- package/commitlint.config.js +3 -0
- package/nuxt.config.ts +539 -0
- package/package.json +101 -0
- package/playground/content/.gitkeep +0 -0
- package/playground/locales/.gitkeep +0 -0
- package/playground/myelophone.dev.ts +45 -0
- package/playground/nuxt.config.ts +105 -0
- package/playground/package.json +5 -0
- package/playground/pages/.gitkeep +0 -0
- package/playground/tsconfig.json +3 -0
- package/playwright.config.ts +23 -0
- package/public/browsers/chrome.svg +77 -0
- package/public/browsers/edge.svg +98 -0
- package/public/browsers/firefox.svg +300 -0
- package/public/favicon.ico +0 -0
- package/public/myelophone_eng.png +0 -0
- package/public/myelophone_eng_white.png +0 -0
- package/spa-loading-template.html +72 -0
- package/tests/home.spec.ts +9 -0
- package/tests/utils.ts +23 -0
- package/tsconfig.json +16 -0
- package/vite/plugins/clean-css.ts +190 -0
- package/vite/plugins/postcss-add-viewportunits.ts +33 -0
- package/vite/plugins/postcss-viewport-fallback.ts +49 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type EntryKey,
|
|
3
|
+
type RefetchOnControl,
|
|
4
|
+
useQuery as useColadaQuery,
|
|
5
|
+
useQueryCache,
|
|
6
|
+
} from '@pinia/colada';
|
|
7
|
+
import { useQuerySyncStore } from '~/stores/querySync';
|
|
8
|
+
|
|
9
|
+
export type QueryKey = EntryKey;
|
|
10
|
+
export type QueryContext = { signal: AbortSignal; queryKey: QueryKey };
|
|
11
|
+
|
|
12
|
+
export interface UseQueryOptions {
|
|
13
|
+
server?: boolean;
|
|
14
|
+
immediate?: boolean;
|
|
15
|
+
share?: boolean;
|
|
16
|
+
staleTime?: number;
|
|
17
|
+
gcTime?: number | false;
|
|
18
|
+
retry?: number | ((attempt: number, error: unknown) => boolean);
|
|
19
|
+
retryDelay?: number | ((attempt: number) => number);
|
|
20
|
+
refetchOnMount?: RefetchOnControl;
|
|
21
|
+
refetchOnWindowFocus?: RefetchOnControl;
|
|
22
|
+
refetchOnReconnect?: RefetchOnControl;
|
|
23
|
+
broadcast?: boolean | string;
|
|
24
|
+
debug?: boolean;
|
|
25
|
+
onError?: (error: unknown) => void;
|
|
26
|
+
onStale?: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const wait = (ms: number) =>
|
|
30
|
+
new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
31
|
+
|
|
32
|
+
type FlightMessage =
|
|
33
|
+
| { type: 'claim'; id: string }
|
|
34
|
+
| { type: 'success'; id: string; data: unknown }
|
|
35
|
+
| { type: 'error'; id: string; message: string };
|
|
36
|
+
|
|
37
|
+
async function crossTabSingleflight<T>(
|
|
38
|
+
key: QueryKey,
|
|
39
|
+
signal: AbortSignal,
|
|
40
|
+
request: () => Promise<T>,
|
|
41
|
+
): Promise<T> {
|
|
42
|
+
if (!('BroadcastChannel' in window)) return request();
|
|
43
|
+
const channel = new BroadcastChannel(`query-flight:${JSON.stringify(key)}`);
|
|
44
|
+
const id: string =
|
|
45
|
+
crypto.randomUUID?.() ?? `${Date.now()}:${Math.random()}`;
|
|
46
|
+
const claims = new Set([id]);
|
|
47
|
+
let settle: ((value: T) => void) | undefined;
|
|
48
|
+
let fail: ((reason: unknown) => void) | undefined;
|
|
49
|
+
const remote = new Promise<T>((resolve, reject) => {
|
|
50
|
+
settle = resolve;
|
|
51
|
+
fail = reject;
|
|
52
|
+
});
|
|
53
|
+
channel.addEventListener(
|
|
54
|
+
'message',
|
|
55
|
+
(event: MessageEvent<FlightMessage>) => {
|
|
56
|
+
const message = event.data;
|
|
57
|
+
if (message.type === 'claim') claims.add(message.id);
|
|
58
|
+
if (message.type === 'success') settle?.(message.data as T);
|
|
59
|
+
if (message.type === 'error') fail?.(new Error(message.message));
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
channel.postMessage({ type: 'claim', id } satisfies FlightMessage);
|
|
63
|
+
await wait(25);
|
|
64
|
+
if (signal.aborted) {
|
|
65
|
+
channel.close();
|
|
66
|
+
throw new DOMException('Query cancelled', 'AbortError');
|
|
67
|
+
}
|
|
68
|
+
if ([...claims].sort()[0] !== id) {
|
|
69
|
+
try {
|
|
70
|
+
return await Promise.race([
|
|
71
|
+
remote,
|
|
72
|
+
wait(30_000).then(() => request()),
|
|
73
|
+
]);
|
|
74
|
+
} finally {
|
|
75
|
+
channel.close();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const data = await request();
|
|
80
|
+
try {
|
|
81
|
+
channel.postMessage({
|
|
82
|
+
type: 'success',
|
|
83
|
+
id,
|
|
84
|
+
data,
|
|
85
|
+
} satisfies FlightMessage);
|
|
86
|
+
} catch {
|
|
87
|
+
// Non-cloneable data cannot be synchronized but the leader still succeeds.
|
|
88
|
+
}
|
|
89
|
+
return data;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
channel.postMessage({
|
|
92
|
+
type: 'error',
|
|
93
|
+
id,
|
|
94
|
+
message: error instanceof Error ? error.message : String(error),
|
|
95
|
+
} satisfies FlightMessage);
|
|
96
|
+
throw error;
|
|
97
|
+
} finally {
|
|
98
|
+
channel.close();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function useQuery<TData>(
|
|
103
|
+
options: {
|
|
104
|
+
key: MaybeRefOrGetter<QueryKey>;
|
|
105
|
+
query: (context: QueryContext) => Promise<TData>;
|
|
106
|
+
} & UseQueryOptions,
|
|
107
|
+
) {
|
|
108
|
+
const {
|
|
109
|
+
server = true,
|
|
110
|
+
immediate = true,
|
|
111
|
+
staleTime = 30_000,
|
|
112
|
+
gcTime = 5 * 60_000,
|
|
113
|
+
retry = 2,
|
|
114
|
+
retryDelay = 500,
|
|
115
|
+
debug = false,
|
|
116
|
+
onError,
|
|
117
|
+
onStale,
|
|
118
|
+
broadcast = false,
|
|
119
|
+
query,
|
|
120
|
+
share: _share,
|
|
121
|
+
...coladaOptions
|
|
122
|
+
} = options;
|
|
123
|
+
|
|
124
|
+
const result = useColadaQuery<TData>({
|
|
125
|
+
...coladaOptions,
|
|
126
|
+
key: options.key,
|
|
127
|
+
enabled: () => immediate && (server || import.meta.client),
|
|
128
|
+
staleTime,
|
|
129
|
+
gcTime,
|
|
130
|
+
query: async (context) => {
|
|
131
|
+
let attempt = 0;
|
|
132
|
+
while (true) {
|
|
133
|
+
try {
|
|
134
|
+
if (debug && import.meta.dev)
|
|
135
|
+
console.info('[useQuery:request]', context.entry.key);
|
|
136
|
+
const request = () => {
|
|
137
|
+
if (debug && import.meta.dev)
|
|
138
|
+
console.info(
|
|
139
|
+
'[useQuery:network]',
|
|
140
|
+
context.entry.key,
|
|
141
|
+
);
|
|
142
|
+
return query({
|
|
143
|
+
signal: context.signal,
|
|
144
|
+
queryKey: context.entry.key,
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
const data =
|
|
148
|
+
broadcast && import.meta.client
|
|
149
|
+
? await crossTabSingleflight(
|
|
150
|
+
context.entry.key,
|
|
151
|
+
context.signal,
|
|
152
|
+
request,
|
|
153
|
+
)
|
|
154
|
+
: await request();
|
|
155
|
+
if (debug && import.meta.dev)
|
|
156
|
+
console.info(
|
|
157
|
+
'[useQuery:success]',
|
|
158
|
+
context.entry.key,
|
|
159
|
+
data,
|
|
160
|
+
);
|
|
161
|
+
return data;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (context.signal.aborted) throw error;
|
|
164
|
+
const shouldRetry =
|
|
165
|
+
typeof retry === 'function'
|
|
166
|
+
? retry(attempt + 1, error)
|
|
167
|
+
: attempt < retry;
|
|
168
|
+
if (!shouldRetry) {
|
|
169
|
+
if (debug && import.meta.dev)
|
|
170
|
+
console.error(
|
|
171
|
+
'[useQuery:error]',
|
|
172
|
+
context.entry.key,
|
|
173
|
+
error,
|
|
174
|
+
);
|
|
175
|
+
onError?.(error);
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
attempt++;
|
|
179
|
+
const delay =
|
|
180
|
+
typeof retryDelay === 'function'
|
|
181
|
+
? retryDelay(attempt)
|
|
182
|
+
: retryDelay * 2 ** (attempt - 1);
|
|
183
|
+
await wait(delay);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
const cache = useQueryCache();
|
|
189
|
+
const isStale = computed(
|
|
190
|
+
() => cache.get(toValue(options.key))?.stale ?? true,
|
|
191
|
+
);
|
|
192
|
+
const isOutdated = computed(
|
|
193
|
+
() => isStale.value && result.data.value !== undefined,
|
|
194
|
+
);
|
|
195
|
+
if (onStale)
|
|
196
|
+
watch(isOutdated, (outdated, wasOutdated) => {
|
|
197
|
+
if (outdated && !wasOutdated) onStale();
|
|
198
|
+
});
|
|
199
|
+
if (broadcast && import.meta.client)
|
|
200
|
+
useQueryBroadcast(
|
|
201
|
+
[toValue(options.key)],
|
|
202
|
+
typeof broadcast === 'string' ? broadcast : 'queries',
|
|
203
|
+
);
|
|
204
|
+
return { ...result, isStale, isOutdated };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function invalidateQuery(key: QueryKey, exact = false) {
|
|
208
|
+
return useQueryCache().invalidateQueries({ key, exact });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function cancelQuery(key: QueryKey, exact = true) {
|
|
212
|
+
useQueryCache().cancelQueries({ key, exact });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function optimisticWrite<TData>(
|
|
216
|
+
key: QueryKey,
|
|
217
|
+
updater: (current: TData | undefined) => TData,
|
|
218
|
+
) {
|
|
219
|
+
const cache = useQueryCache();
|
|
220
|
+
const previous = cache.getQueryData<TData>(key);
|
|
221
|
+
cache.setQueryData<TData>(key, updater(previous));
|
|
222
|
+
return () => cache.setQueryData<TData>(key, previous as TData);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function useQueryBroadcast(
|
|
226
|
+
keys: readonly QueryKey[],
|
|
227
|
+
channel = 'queries',
|
|
228
|
+
) {
|
|
229
|
+
if (!import.meta.client) return () => {};
|
|
230
|
+
const cache = useQueryCache();
|
|
231
|
+
const sync = useQuerySyncStore(useNuxtApp().$pinia);
|
|
232
|
+
const ids = keys.map((key) => JSON.stringify(key));
|
|
233
|
+
let applyingRemote = false;
|
|
234
|
+
let lastLocalSnapshot = '';
|
|
235
|
+
|
|
236
|
+
const stopLocal = watchEffect(() => {
|
|
237
|
+
if (applyingRemote) return;
|
|
238
|
+
const next = Object.fromEntries(
|
|
239
|
+
keys.map((key, index) => [ids[index]!, cache.getQueryData(key)]),
|
|
240
|
+
);
|
|
241
|
+
const serialized = JSON.stringify(next);
|
|
242
|
+
if (serialized !== JSON.stringify(sync.records)) {
|
|
243
|
+
lastLocalSnapshot = serialized;
|
|
244
|
+
sync.records = next;
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const stopRemote = watch(
|
|
249
|
+
() => sync.records,
|
|
250
|
+
(records) => {
|
|
251
|
+
if (JSON.stringify(records) === lastLocalSnapshot) return;
|
|
252
|
+
applyingRemote = true;
|
|
253
|
+
try {
|
|
254
|
+
keys.forEach((key, index) => {
|
|
255
|
+
const value = records[ids[index]!];
|
|
256
|
+
if (value !== undefined) {
|
|
257
|
+
cache.cancelQueries({ key, exact: true });
|
|
258
|
+
cache.setQueryData(key, value);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
} finally {
|
|
262
|
+
applyingRemote = false;
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
{ deep: true },
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const stopBroadcast = useStoreBroadcast(
|
|
269
|
+
sync as unknown as Parameters<typeof useStoreBroadcast>[0],
|
|
270
|
+
{
|
|
271
|
+
keys: ['records'],
|
|
272
|
+
channel: `pinia:${channel}`,
|
|
273
|
+
},
|
|
274
|
+
);
|
|
275
|
+
const stop = () => {
|
|
276
|
+
stopLocal();
|
|
277
|
+
stopRemote();
|
|
278
|
+
stopBroadcast();
|
|
279
|
+
ids.forEach((id) => delete sync.records[id]);
|
|
280
|
+
};
|
|
281
|
+
if (getCurrentInstance()) onScopeDispose(stop);
|
|
282
|
+
return stop;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export async function* readJsonStream<T>(
|
|
286
|
+
response: Response,
|
|
287
|
+
): AsyncGenerator<T> {
|
|
288
|
+
if (!response.body) throw new TypeError('Response has no readable body');
|
|
289
|
+
const reader = response.body
|
|
290
|
+
.pipeThrough(new TextDecoderStream())
|
|
291
|
+
.getReader();
|
|
292
|
+
let buffer = '';
|
|
293
|
+
try {
|
|
294
|
+
while (true) {
|
|
295
|
+
const { done, value } = await reader.read();
|
|
296
|
+
buffer += value ?? '';
|
|
297
|
+
const lines = buffer.split(/\r?\n/);
|
|
298
|
+
buffer = lines.pop() ?? '';
|
|
299
|
+
for (const line of lines)
|
|
300
|
+
if (line.trim()) yield JSON.parse(line) as T;
|
|
301
|
+
if (done) break;
|
|
302
|
+
}
|
|
303
|
+
if (buffer.trim()) yield JSON.parse(buffer) as T;
|
|
304
|
+
} finally {
|
|
305
|
+
reader.releaseLock();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { CSSProperties } from 'vue';
|
|
2
|
+
|
|
3
|
+
export type DeepPartial<T> =
|
|
4
|
+
T extends Array<infer U>
|
|
5
|
+
? U[]
|
|
6
|
+
: T extends object
|
|
7
|
+
? { [K in keyof T]?: DeepPartial<T[K]> }
|
|
8
|
+
: T;
|
|
9
|
+
|
|
10
|
+
export type ReadyContentByLocale<T> = Record<string, DeepPartial<T>>;
|
|
11
|
+
|
|
12
|
+
export interface ReadySeoContent {
|
|
13
|
+
seo: {
|
|
14
|
+
title: string;
|
|
15
|
+
description: string;
|
|
16
|
+
ogTitle?: string;
|
|
17
|
+
ogDescription?: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface UseReadyPageOptions<T extends ReadySeoContent> {
|
|
22
|
+
contentByLocale: Record<string, T>;
|
|
23
|
+
content?: MaybeRefOrGetter<DeepPartial<T> | undefined>;
|
|
24
|
+
overridesByLocale?: MaybeRefOrGetter<ReadyContentByLocale<T> | undefined>;
|
|
25
|
+
fallbackLocale?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface UseReadyPageResult<T> {
|
|
29
|
+
activeLocale: ComputedRef<string>;
|
|
30
|
+
availableLocales: ComputedRef<string[]>;
|
|
31
|
+
currentContent: ComputedRef<T>;
|
|
32
|
+
resolvedContentByLocale: ComputedRef<Record<string, T>>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function mergeReadyContent<T>(base: T, override?: DeepPartial<T>): T {
|
|
36
|
+
if (override === undefined) return base;
|
|
37
|
+
|
|
38
|
+
if (Array.isArray(base)) {
|
|
39
|
+
return (Array.isArray(override) ? override : base) as T;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (base && typeof base === 'object') {
|
|
43
|
+
const result = { ...base } as Record<string, unknown>;
|
|
44
|
+
const source = (override || {}) as Record<string, unknown>;
|
|
45
|
+
|
|
46
|
+
for (const key of Object.keys(source)) {
|
|
47
|
+
const value = source[key];
|
|
48
|
+
if (value === undefined) continue;
|
|
49
|
+
|
|
50
|
+
const initial = (base as Record<string, unknown>)[key];
|
|
51
|
+
result[key] =
|
|
52
|
+
initial &&
|
|
53
|
+
typeof initial === 'object' &&
|
|
54
|
+
!Array.isArray(initial) &&
|
|
55
|
+
value &&
|
|
56
|
+
typeof value === 'object' &&
|
|
57
|
+
!Array.isArray(value)
|
|
58
|
+
? mergeReadyContent(initial, value)
|
|
59
|
+
: value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return result as T;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (override ?? base) as T;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function readyCssVariables<T extends object>(
|
|
69
|
+
values: T,
|
|
70
|
+
prefix = 'ready',
|
|
71
|
+
): CSSProperties {
|
|
72
|
+
return Object.fromEntries(
|
|
73
|
+
Object.entries(values as Record<string, string | number | undefined>)
|
|
74
|
+
.filter(
|
|
75
|
+
(entry): entry is [string, string | number] =>
|
|
76
|
+
entry[1] !== undefined,
|
|
77
|
+
)
|
|
78
|
+
.map(([key, value]) => [
|
|
79
|
+
`--${prefix}-${key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`,
|
|
80
|
+
String(value),
|
|
81
|
+
]),
|
|
82
|
+
) as CSSProperties;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function useReadyPage<T extends ReadySeoContent>(
|
|
86
|
+
options: UseReadyPageOptions<T>,
|
|
87
|
+
): UseReadyPageResult<T> {
|
|
88
|
+
const fallbackLocale = (options.fallbackLocale || 'en').toLowerCase();
|
|
89
|
+
const settingsStore = useSettingsStore(useNuxtApp().$pinia);
|
|
90
|
+
const myelophoneConfig = useMyelophoneConfig();
|
|
91
|
+
|
|
92
|
+
const activeLocale = computed(() =>
|
|
93
|
+
(
|
|
94
|
+
settingsStore.currentLocale ||
|
|
95
|
+
settingsStore.defaultLocale ||
|
|
96
|
+
fallbackLocale
|
|
97
|
+
).toLowerCase(),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const configuredLocales = computed(() => {
|
|
101
|
+
const multi18n = myelophoneConfig.multi18n as
|
|
102
|
+
{ locales?: string[] } | undefined;
|
|
103
|
+
return multi18n?.locales || [];
|
|
104
|
+
});
|
|
105
|
+
const content = computed(() => toValue(options.content));
|
|
106
|
+
const overridesByLocale = computed(
|
|
107
|
+
() => toValue(options.overridesByLocale) || {},
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const availableLocales = computed(() => [
|
|
111
|
+
...new Set(
|
|
112
|
+
[
|
|
113
|
+
...configuredLocales.value,
|
|
114
|
+
...Object.keys(options.contentByLocale),
|
|
115
|
+
...Object.keys(overridesByLocale.value),
|
|
116
|
+
].map((locale) => locale.toLowerCase()),
|
|
117
|
+
),
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
const resolvedContentByLocale = computed<Record<string, T>>(() =>
|
|
121
|
+
Object.fromEntries(
|
|
122
|
+
availableLocales.value.map((locale) => {
|
|
123
|
+
const baseLocale = locale.split('-')[0] || fallbackLocale;
|
|
124
|
+
const defaults =
|
|
125
|
+
options.contentByLocale[locale] ||
|
|
126
|
+
options.contentByLocale[baseLocale] ||
|
|
127
|
+
options.contentByLocale[fallbackLocale];
|
|
128
|
+
|
|
129
|
+
if (!defaults) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`[ready] Missing fallback content for locale "${fallbackLocale}"`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const localeOverride =
|
|
136
|
+
overridesByLocale.value[locale] ||
|
|
137
|
+
overridesByLocale.value[baseLocale];
|
|
138
|
+
|
|
139
|
+
return [
|
|
140
|
+
locale,
|
|
141
|
+
mergeReadyContent(
|
|
142
|
+
mergeReadyContent(defaults, content.value),
|
|
143
|
+
localeOverride,
|
|
144
|
+
),
|
|
145
|
+
];
|
|
146
|
+
}),
|
|
147
|
+
),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const currentContent = computed(() => {
|
|
151
|
+
const baseLocale = activeLocale.value.split('-')[0] || fallbackLocale;
|
|
152
|
+
const content =
|
|
153
|
+
resolvedContentByLocale.value[activeLocale.value] ||
|
|
154
|
+
resolvedContentByLocale.value[baseLocale] ||
|
|
155
|
+
resolvedContentByLocale.value[fallbackLocale];
|
|
156
|
+
|
|
157
|
+
if (!content) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`[ready] Unable to resolve content for locale "${activeLocale.value}"`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return content;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
useSeoMeta({
|
|
167
|
+
title: () => currentContent.value.seo.title,
|
|
168
|
+
description: () => currentContent.value.seo.description,
|
|
169
|
+
ogTitle: () =>
|
|
170
|
+
currentContent.value.seo.ogTitle || currentContent.value.seo.title,
|
|
171
|
+
ogDescription: () =>
|
|
172
|
+
currentContent.value.seo.ogDescription ||
|
|
173
|
+
currentContent.value.seo.description,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
activeLocale,
|
|
178
|
+
availableLocales,
|
|
179
|
+
currentContent,
|
|
180
|
+
resolvedContentByLocale,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_SAFE_LIST_PATH = path.join(
|
|
5
|
+
'.nuxt',
|
|
6
|
+
'i18n-safelist.generated.json',
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
let safeListSessionStarted = false;
|
|
10
|
+
|
|
11
|
+
const isTranslationKey = (value: unknown): value is string =>
|
|
12
|
+
typeof value === 'string' &&
|
|
13
|
+
/^[a-zA-Z0-9/_-]+(?:\.[a-zA-Z0-9/_-]+)+$/.test(value);
|
|
14
|
+
|
|
15
|
+
const collectKeys = (
|
|
16
|
+
source: unknown,
|
|
17
|
+
keys = new Set<string>(),
|
|
18
|
+
allowDirectString = true,
|
|
19
|
+
) => {
|
|
20
|
+
if (!source) return keys;
|
|
21
|
+
|
|
22
|
+
if (allowDirectString && isTranslationKey(source)) {
|
|
23
|
+
keys.add(source);
|
|
24
|
+
return keys;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (Array.isArray(source)) {
|
|
28
|
+
source.forEach((item) => collectKeys(item, keys, true));
|
|
29
|
+
return keys;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (typeof source !== 'object') return keys;
|
|
33
|
+
|
|
34
|
+
Object.entries(source).forEach(([key, value]) => {
|
|
35
|
+
if (key !== 'loadKey' && key.endsWith('Key')) {
|
|
36
|
+
if (isTranslationKey(value)) keys.add(value);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (key.endsWith('Keys') && Array.isArray(value)) {
|
|
41
|
+
value.forEach((item) => {
|
|
42
|
+
if (isTranslationKey(item)) keys.add(item);
|
|
43
|
+
});
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
collectKeys(value, keys, false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return keys;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const useSafeList = (
|
|
54
|
+
source: unknown,
|
|
55
|
+
{
|
|
56
|
+
safelistPath = DEFAULT_SAFE_LIST_PATH,
|
|
57
|
+
cwd = process.cwd(),
|
|
58
|
+
}: {
|
|
59
|
+
safelistPath?: string;
|
|
60
|
+
cwd?: string;
|
|
61
|
+
} = {},
|
|
62
|
+
) => {
|
|
63
|
+
const outputPath = path.isAbsolute(safelistPath)
|
|
64
|
+
? safelistPath
|
|
65
|
+
: path.resolve(cwd, safelistPath);
|
|
66
|
+
const keys = new Set<string>();
|
|
67
|
+
|
|
68
|
+
if (safeListSessionStarted && fs.existsSync(outputPath)) {
|
|
69
|
+
try {
|
|
70
|
+
(
|
|
71
|
+
JSON.parse(fs.readFileSync(outputPath, 'utf-8')) as unknown[]
|
|
72
|
+
).forEach((key) => {
|
|
73
|
+
if (isTranslationKey(key)) keys.add(key);
|
|
74
|
+
});
|
|
75
|
+
} catch (e) {}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
safeListSessionStarted = true;
|
|
79
|
+
collectKeys(source, keys);
|
|
80
|
+
|
|
81
|
+
const sortedKeys = Array.from(keys).sort();
|
|
82
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
83
|
+
fs.writeFileSync(
|
|
84
|
+
outputPath,
|
|
85
|
+
`${JSON.stringify(sortedKeys, null, 2)}\n`,
|
|
86
|
+
'utf-8',
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return sortedKeys;
|
|
90
|
+
};
|