@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,42 @@
|
|
|
1
|
+
// @ts-expect-error generated by default-sitemap module
|
|
2
|
+
import urls from '#default-sitemap/urls.mjs';
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler((event) => {
|
|
5
|
+
const config = useRuntimeConfig(event).myelophone;
|
|
6
|
+
|
|
7
|
+
const configuredSiteUrl = String(config.siteDomain ?? '').replace(
|
|
8
|
+
/\/$/,
|
|
9
|
+
'',
|
|
10
|
+
);
|
|
11
|
+
const siteUrl = configuredSiteUrl || getRequestURL(event).origin;
|
|
12
|
+
|
|
13
|
+
setResponseHeader(event, 'content-type', 'application/xml; charset=utf-8');
|
|
14
|
+
|
|
15
|
+
return createSitemapXml(siteUrl, urls);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function createSitemapXml(siteUrl: string, urls: string[]): string {
|
|
19
|
+
const body = urls
|
|
20
|
+
.map((path) => {
|
|
21
|
+
const loc = escapeXml(`${siteUrl}${path}`);
|
|
22
|
+
|
|
23
|
+
return `<url><loc>${loc}</loc></url>`;
|
|
24
|
+
})
|
|
25
|
+
.join('');
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
'<?xml version="1.0" encoding="UTF-8"?>' +
|
|
29
|
+
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' +
|
|
30
|
+
body +
|
|
31
|
+
'</urlset>'
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function escapeXml(value: string): string {
|
|
36
|
+
return value
|
|
37
|
+
.replace(/&/g, '&')
|
|
38
|
+
.replace(/</g, '<')
|
|
39
|
+
.replace(/>/g, '>')
|
|
40
|
+
.replace(/"/g, '"')
|
|
41
|
+
.replace(/'/g, ''');
|
|
42
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { addComponent, addTemplate, defineNuxtModule } from '@nuxt/kit';
|
|
2
|
+
|
|
3
|
+
interface FullscreenPreloaderConfig {
|
|
4
|
+
component?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const COMPONENT_NAME_PATTERN = /^[A-Z][A-Za-z0-9_]*$/;
|
|
8
|
+
|
|
9
|
+
export default defineNuxtModule({
|
|
10
|
+
meta: {
|
|
11
|
+
name: '@myelophone/nuxt-fullscreen-preloader',
|
|
12
|
+
},
|
|
13
|
+
setup(_options, nuxt) {
|
|
14
|
+
const configured = nuxt.options.myelophone?.pageFullscreenPreloader as
|
|
15
|
+
FullscreenPreloaderConfig | undefined;
|
|
16
|
+
|
|
17
|
+
const componentName = configured?.component;
|
|
18
|
+
|
|
19
|
+
const validComponentName =
|
|
20
|
+
componentName && COMPONENT_NAME_PATTERN.test(componentName)
|
|
21
|
+
? componentName
|
|
22
|
+
: undefined;
|
|
23
|
+
|
|
24
|
+
if (componentName && !validComponentName) {
|
|
25
|
+
console.warn(
|
|
26
|
+
`[pageFullscreenPreloader] Ignored invalid component name: ${componentName}`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const wrapper = addTemplate({
|
|
31
|
+
filename: 'myelophone/fullscreen-preloader-content.vue',
|
|
32
|
+
getContents: () =>
|
|
33
|
+
validComponentName
|
|
34
|
+
? `<template><${validComponentName} v-bind="$attrs" /></template>\n`
|
|
35
|
+
: '<template></template>\n',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
addComponent({
|
|
39
|
+
name: 'PageFullscreenPreloaderConfiguredContent',
|
|
40
|
+
filePath: wrapper.dst,
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<IfModule mod_rewrite.c>
|
|
2
|
+
RewriteEngine On
|
|
3
|
+
RewriteBase /
|
|
4
|
+
|
|
5
|
+
RewriteCond %{REQUEST_FILENAME} !-f
|
|
6
|
+
RewriteCond %{REQUEST_FILENAME} !-d
|
|
7
|
+
RewriteCond %{REQUEST_FILENAME}.html -f
|
|
8
|
+
RewriteRule ^(.+?)/?$ $1.html [L]
|
|
9
|
+
</IfModule>
|
|
10
|
+
|
|
11
|
+
<IfModule mod_expires.c>
|
|
12
|
+
ExpiresActive On
|
|
13
|
+
ExpiresByType text/html "access plus 5 minutes"
|
|
14
|
+
ExpiresByType text/css "access plus 1 year"
|
|
15
|
+
ExpiresByType application/javascript "access plus 1 year"
|
|
16
|
+
ExpiresByType image/webp "access plus 1 year"
|
|
17
|
+
ExpiresByType image/avif "access plus 1 year"
|
|
18
|
+
ExpiresByType image/png "access plus 1 year"
|
|
19
|
+
ExpiresByType image/jpeg "access plus 1 year"
|
|
20
|
+
ExpiresByType image/svg+xml "access plus 1 year"
|
|
21
|
+
ExpiresByType font/woff2 "access plus 1 year"
|
|
22
|
+
</IfModule>
|
|
23
|
+
|
|
24
|
+
<IfModule mod_headers.c>
|
|
25
|
+
<FilesMatch "\.html$">
|
|
26
|
+
Header set Cache-Control "public, max-age=300, must-revalidate"
|
|
27
|
+
</FilesMatch>
|
|
28
|
+
<FilesMatch "\.(css|js|woff2|png|jpe?g|webp|avif|svg)$">
|
|
29
|
+
Header set Cache-Control "public, max-age=31536000, immutable"
|
|
30
|
+
</FilesMatch>
|
|
31
|
+
Header always set X-Content-Type-Options "nosniff"
|
|
32
|
+
Header always set X-Frame-Options "SAMEORIGIN"
|
|
33
|
+
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
|
34
|
+
</IfModule>
|
|
35
|
+
|
|
36
|
+
<IfModule mod_deflate.c>
|
|
37
|
+
AddOutputFilterByType DEFLATE text/html text/css application/javascript application/json image/svg+xml
|
|
38
|
+
</IfModule>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineNuxtModule } from '@nuxt/kit';
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export default defineNuxtModule({
|
|
6
|
+
meta: { name: 'nuxt-static-htaccess' },
|
|
7
|
+
setup(_, nuxt) {
|
|
8
|
+
nuxt.hook('nitro:build:public-assets', () => {
|
|
9
|
+
if (!process.env.NUXT_STATIC) return;
|
|
10
|
+
|
|
11
|
+
const src = join(__dirname, '.htaccess');
|
|
12
|
+
if (!existsSync(src)) return;
|
|
13
|
+
|
|
14
|
+
const destDir = join(nuxt.options.rootDir, '.output/public');
|
|
15
|
+
if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
|
|
16
|
+
|
|
17
|
+
const dest = join(destDir, '.htaccess');
|
|
18
|
+
copyFileSync(src, dest);
|
|
19
|
+
|
|
20
|
+
console.info('[htaccess] .htaccess added to ', destDir);
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
});
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const I18N_SAFE_LIST_READY_KEY = '__i18nSafeListReady';
|
|
5
|
+
const I18N_SAFE_LIST_KEYS_KEY = '__i18nSafeListKeys';
|
|
6
|
+
|
|
7
|
+
function getDefaultSafelistPath() {
|
|
8
|
+
return path.join('.nuxt', 'i18n-safelist.generated.json');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function i18nTreeShaker(options = {}) {
|
|
12
|
+
const {
|
|
13
|
+
localesDir = 'locales',
|
|
14
|
+
safelistPath,
|
|
15
|
+
generatedSafelistPath = getDefaultSafelistPath(),
|
|
16
|
+
scanDirs,
|
|
17
|
+
} = options;
|
|
18
|
+
const usedKeys = new Set();
|
|
19
|
+
|
|
20
|
+
function getFiles(dir) {
|
|
21
|
+
if (!fs.existsSync(dir)) return [];
|
|
22
|
+
const dirents = fs.readdirSync(dir, { withFileTypes: true });
|
|
23
|
+
const files = dirents.map((dirent) => {
|
|
24
|
+
const res = path.resolve(dir, dirent.name);
|
|
25
|
+
return dirent.isDirectory() ? getFiles(res) : res;
|
|
26
|
+
});
|
|
27
|
+
return files.flat();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function setDeep(obj, pathStr, value) {
|
|
31
|
+
const keys = pathStr.split('.');
|
|
32
|
+
let current = obj;
|
|
33
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
34
|
+
const key = keys[i];
|
|
35
|
+
if (!current[key]) current[key] = {};
|
|
36
|
+
current = current[key];
|
|
37
|
+
}
|
|
38
|
+
current[keys[keys.length - 1]] = value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getDeep(obj, pathStr) {
|
|
42
|
+
if (!pathStr) return obj;
|
|
43
|
+
return pathStr.split('.').reduce((acc, key) => acc && acc[key], obj);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readSafelistFile(filePath) {
|
|
47
|
+
if (!filePath || !fs.existsSync(filePath)) return [];
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
51
|
+
const parsed = JSON.parse(content);
|
|
52
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
53
|
+
} catch (e) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function writeSafelistFile(filePath, keys) {
|
|
59
|
+
if (!filePath) return;
|
|
60
|
+
|
|
61
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
62
|
+
fs.writeFileSync(
|
|
63
|
+
filePath,
|
|
64
|
+
`${JSON.stringify(Array.from(keys).sort(), null, 2)}\n`,
|
|
65
|
+
'utf-8',
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
name: 'vite-plugin-i18n-shaker',
|
|
71
|
+
enforce: 'pre',
|
|
72
|
+
apply: 'build',
|
|
73
|
+
|
|
74
|
+
async buildStart() {
|
|
75
|
+
usedKeys.clear();
|
|
76
|
+
|
|
77
|
+
const safeListReady = globalThis[I18N_SAFE_LIST_READY_KEY];
|
|
78
|
+
if (safeListReady && typeof safeListReady.then === 'function') {
|
|
79
|
+
await safeListReady;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const resolvedGeneratedSafelistPath = path.isAbsolute(
|
|
83
|
+
generatedSafelistPath,
|
|
84
|
+
)
|
|
85
|
+
? generatedSafelistPath
|
|
86
|
+
: path.resolve(process.cwd(), generatedSafelistPath);
|
|
87
|
+
const resolvedManualSafelistPath = safelistPath
|
|
88
|
+
? path.isAbsolute(safelistPath)
|
|
89
|
+
? safelistPath
|
|
90
|
+
: path.resolve(process.cwd(), safelistPath)
|
|
91
|
+
: null;
|
|
92
|
+
const registeredKeys = globalThis[I18N_SAFE_LIST_KEYS_KEY];
|
|
93
|
+
const generatedKeys = new Set(
|
|
94
|
+
registeredKeys instanceof Set
|
|
95
|
+
? Array.from(registeredKeys)
|
|
96
|
+
: Array.isArray(registeredKeys)
|
|
97
|
+
? registeredKeys
|
|
98
|
+
: [],
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
readSafelistFile(resolvedManualSafelistPath).forEach((key) => {
|
|
102
|
+
if (typeof key === 'string') usedKeys.add(key);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (generatedKeys.size) {
|
|
106
|
+
writeSafelistFile(resolvedGeneratedSafelistPath, generatedKeys);
|
|
107
|
+
generatedKeys.forEach((key) => usedKeys.add(key));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const rootDir = process.cwd();
|
|
111
|
+
const defaultScanDirs = [
|
|
112
|
+
path.resolve(rootDir, 'app'),
|
|
113
|
+
...(process.env.NUXT_PLAYGROUND === 'true'
|
|
114
|
+
? [path.resolve(rootDir, 'playground')]
|
|
115
|
+
: []),
|
|
116
|
+
];
|
|
117
|
+
const projectLayers = scanDirs || defaultScanDirs;
|
|
118
|
+
|
|
119
|
+
projectLayers.forEach((layerPath) => {
|
|
120
|
+
if (fs.existsSync(layerPath)) {
|
|
121
|
+
getFiles(layerPath).forEach((file) => {
|
|
122
|
+
if (/\.(vue|ts|js|mjs)$/.test(file)) {
|
|
123
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
124
|
+
|
|
125
|
+
const regex =
|
|
126
|
+
/(?:(?:\$t|t|tn)\s*\(\s*['"`])([a-zA-Z0-9/_.-]+)(?=['"`]\s*[,)])/g;
|
|
127
|
+
|
|
128
|
+
const matches = content.matchAll(regex);
|
|
129
|
+
for (const match of matches) {
|
|
130
|
+
if (match[1]) usedKeys.add(match[1]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const keepRegex =
|
|
134
|
+
/@i18n-keep\s+([a-zA-Z0-9/_.-]+)/g;
|
|
135
|
+
const keepMatches = content.matchAll(keepRegex);
|
|
136
|
+
for (const match of keepMatches) {
|
|
137
|
+
if (match[1]) usedKeys.add(match[1]);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
transform(code, id) {
|
|
146
|
+
const cleanId = id.split('?')[0].replace(/\\/g, '/');
|
|
147
|
+
|
|
148
|
+
if (
|
|
149
|
+
cleanId.includes(`/${localesDir}/`) &&
|
|
150
|
+
cleanId.endsWith('.json')
|
|
151
|
+
) {
|
|
152
|
+
try {
|
|
153
|
+
const rawJson = JSON.parse(code);
|
|
154
|
+
const filteredJson = {};
|
|
155
|
+
let keptCount = 0;
|
|
156
|
+
|
|
157
|
+
const parts = cleanId
|
|
158
|
+
.split(`/${localesDir}/`)[1]
|
|
159
|
+
.split('/');
|
|
160
|
+
const namespace = parts
|
|
161
|
+
.slice(1)
|
|
162
|
+
.join('/')
|
|
163
|
+
.replace('.json', '');
|
|
164
|
+
|
|
165
|
+
usedKeys.forEach((fullKey) => {
|
|
166
|
+
if (
|
|
167
|
+
fullKey.startsWith(`${namespace}.`) ||
|
|
168
|
+
fullKey === namespace
|
|
169
|
+
) {
|
|
170
|
+
const internalKey =
|
|
171
|
+
fullKey === namespace
|
|
172
|
+
? ''
|
|
173
|
+
: fullKey.slice(namespace.length + 1);
|
|
174
|
+
|
|
175
|
+
const value = getDeep(rawJson, internalKey);
|
|
176
|
+
if (value !== undefined) {
|
|
177
|
+
if (typeof value === 'string') {
|
|
178
|
+
setDeep(filteredJson, internalKey, value);
|
|
179
|
+
keptCount++;
|
|
180
|
+
} else if (typeof value === 'object') {
|
|
181
|
+
setDeep(filteredJson, internalKey, value);
|
|
182
|
+
keptCount += Object.keys(value).length;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const pathParts = internalKey.split('.');
|
|
187
|
+
const lastPart = pathParts.pop();
|
|
188
|
+
const parentPath = pathParts.join('.');
|
|
189
|
+
const parentObj = getDeep(rawJson, parentPath);
|
|
190
|
+
|
|
191
|
+
if (parentObj && typeof parentObj === 'object') {
|
|
192
|
+
Object.keys(parentObj).forEach((k) => {
|
|
193
|
+
if (k.startsWith(`${lastPart}_`)) {
|
|
194
|
+
const pluralPath = parentPath
|
|
195
|
+
? `${parentPath}.${k}`
|
|
196
|
+
: k;
|
|
197
|
+
setDeep(
|
|
198
|
+
filteredJson,
|
|
199
|
+
pluralPath,
|
|
200
|
+
parentObj[k],
|
|
201
|
+
);
|
|
202
|
+
keptCount++;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
code: JSON.stringify(filteredJson),
|
|
211
|
+
map: null,
|
|
212
|
+
};
|
|
213
|
+
} catch (e) {
|
|
214
|
+
console.error(`[i18n-shaker] Error in ${id}:`, e);
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineNuxtModule,
|
|
3
|
+
createResolver,
|
|
4
|
+
addComponentsDir,
|
|
5
|
+
addImportsDir,
|
|
6
|
+
} from 'nuxt/kit';
|
|
7
|
+
|
|
8
|
+
export interface MyelophoneNuxtModuleOptions {
|
|
9
|
+
defaultLocale: string;
|
|
10
|
+
locales: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default defineNuxtModule<MyelophoneNuxtModuleOptions>({
|
|
14
|
+
meta: {
|
|
15
|
+
name: 'multi18n-module',
|
|
16
|
+
configKey: 'multi18n',
|
|
17
|
+
compatibility: { nuxt: '^4.0.0' },
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
defaults: {
|
|
21
|
+
defaultLocale: 'en',
|
|
22
|
+
locales: ['en'],
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
async setup(options, nuxt) {
|
|
26
|
+
const localOptions = { ...options };
|
|
27
|
+
const { resolve } = createResolver(import.meta.url);
|
|
28
|
+
|
|
29
|
+
nuxt.hooks.hook('pages:extend', (pages) => {
|
|
30
|
+
const { locales, defaultLocale } = localOptions;
|
|
31
|
+
const otherLocales = locales.filter((l) => l !== defaultLocale);
|
|
32
|
+
|
|
33
|
+
const newPages = [];
|
|
34
|
+
const existingPaths = new Set(pages.map((p) => p.path));
|
|
35
|
+
|
|
36
|
+
for (const page of pages) {
|
|
37
|
+
if (page.meta?.i18n === false) continue;
|
|
38
|
+
|
|
39
|
+
if (page.file?.includes('[...all]')) continue;
|
|
40
|
+
|
|
41
|
+
for (const lang of otherLocales) {
|
|
42
|
+
if (page.path === '/' && lang === defaultLocale) continue;
|
|
43
|
+
|
|
44
|
+
const localizedPath =
|
|
45
|
+
page.path === '/' ? `/${lang}` : `/${lang}${page.path}`;
|
|
46
|
+
|
|
47
|
+
if (existingPaths.has(localizedPath)) continue;
|
|
48
|
+
existingPaths.add(localizedPath);
|
|
49
|
+
|
|
50
|
+
newPages.push({
|
|
51
|
+
...page,
|
|
52
|
+
path: localizedPath,
|
|
53
|
+
file: page.file,
|
|
54
|
+
name: `${lang}-${page.name || page.path.replace(/\W+/g, '_')}`,
|
|
55
|
+
aliasOf: page.path,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
pages.push(...newPages);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
addComponentsDir({
|
|
64
|
+
path: resolve('runtime/components'),
|
|
65
|
+
pathPrefix: false,
|
|
66
|
+
});
|
|
67
|
+
addImportsDir(resolve('runtime/composables'));
|
|
68
|
+
addImportsDir(resolve('runtime/utils'));
|
|
69
|
+
addImportsDir(resolve('runtime/stores'));
|
|
70
|
+
},
|
|
71
|
+
});
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { defineNuxtModule, extendViteConfig, useLogger } from '@nuxt/kit';
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
MyelophoneClientConfig,
|
|
5
|
+
MyelophoneConfig,
|
|
6
|
+
MyelophoneMulti18nConfig,
|
|
7
|
+
} from '~/types/myelophone';
|
|
8
|
+
|
|
9
|
+
const isSSG = process.env.NUXT_STATIC === 'true';
|
|
10
|
+
const isSSRStreaming = process.env.NUXT_SSR_STREAMING === 'true';
|
|
11
|
+
|
|
12
|
+
const setSSRStreaming = (
|
|
13
|
+
experimental: Record<string, any>,
|
|
14
|
+
enabled: boolean,
|
|
15
|
+
) => {
|
|
16
|
+
const current = experimental.ssrStreaming;
|
|
17
|
+
|
|
18
|
+
experimental.ssrStreaming = {
|
|
19
|
+
...(current && typeof current === 'object' ? current : {}),
|
|
20
|
+
enabled,
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const enableDefaultRouteStreaming = (routeRules: Record<string, any>) => {
|
|
25
|
+
const current = routeRules['/**'];
|
|
26
|
+
|
|
27
|
+
routeRules['/**'] = {
|
|
28
|
+
...(current && typeof current === 'object' ? current : {}),
|
|
29
|
+
streaming:
|
|
30
|
+
current && typeof current === 'object' && 'streaming' in current
|
|
31
|
+
? current.streaming
|
|
32
|
+
: true,
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const defaultCookieScripts = {
|
|
37
|
+
necessary: [],
|
|
38
|
+
analytics: [],
|
|
39
|
+
marketing: [],
|
|
40
|
+
functional: [],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const defaultMyelophoneConfig: MyelophoneConfig = {
|
|
44
|
+
siteDomain: 'http://localhost:3000',
|
|
45
|
+
cookieScripts: defaultCookieScripts,
|
|
46
|
+
cookieControl: {
|
|
47
|
+
enabled: true,
|
|
48
|
+
},
|
|
49
|
+
bundleTranslations: true,
|
|
50
|
+
splitCss: false,
|
|
51
|
+
stores: {
|
|
52
|
+
cart: false,
|
|
53
|
+
user: false,
|
|
54
|
+
},
|
|
55
|
+
frankfurterCurrencies: [],
|
|
56
|
+
frankfurterBaseCurrency: 'USD',
|
|
57
|
+
creativeCursor: false,
|
|
58
|
+
siteSearch: {
|
|
59
|
+
strategy: 'client',
|
|
60
|
+
endpoint: '',
|
|
61
|
+
minQueryLength: 2,
|
|
62
|
+
limit: 10,
|
|
63
|
+
operator: 'and',
|
|
64
|
+
queryParam: 'search',
|
|
65
|
+
modeParam: 'searchMode',
|
|
66
|
+
operatorParam: 'searchOperator',
|
|
67
|
+
urlMode: 'search',
|
|
68
|
+
consumeUrl: false,
|
|
69
|
+
enabled: true,
|
|
70
|
+
},
|
|
71
|
+
tally: {
|
|
72
|
+
domain: 'tally.so',
|
|
73
|
+
},
|
|
74
|
+
noindex: false,
|
|
75
|
+
blog: {
|
|
76
|
+
blogEnabled: true,
|
|
77
|
+
postsLayout: 'default-blog',
|
|
78
|
+
},
|
|
79
|
+
ssrStream: false,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const resolveMyelophoneConfig = (
|
|
83
|
+
config?: Partial<MyelophoneConfig>,
|
|
84
|
+
): MyelophoneConfig => ({
|
|
85
|
+
...defaultMyelophoneConfig,
|
|
86
|
+
...config,
|
|
87
|
+
cookieScripts:
|
|
88
|
+
config?.cookieScripts ?? defaultMyelophoneConfig.cookieScripts,
|
|
89
|
+
cookieControl: {
|
|
90
|
+
...defaultMyelophoneConfig.cookieControl,
|
|
91
|
+
...config?.cookieControl,
|
|
92
|
+
enabled:
|
|
93
|
+
config?.cookieControl?.enabled ??
|
|
94
|
+
defaultMyelophoneConfig.cookieControl?.enabled ??
|
|
95
|
+
true,
|
|
96
|
+
},
|
|
97
|
+
stores: {
|
|
98
|
+
...defaultMyelophoneConfig.stores,
|
|
99
|
+
...config?.stores,
|
|
100
|
+
},
|
|
101
|
+
siteSearch: {
|
|
102
|
+
...defaultMyelophoneConfig.siteSearch,
|
|
103
|
+
...config?.siteSearch,
|
|
104
|
+
},
|
|
105
|
+
pageFullscreenPreloader:
|
|
106
|
+
config?.pageFullscreenPreloader ??
|
|
107
|
+
defaultMyelophoneConfig.pageFullscreenPreloader,
|
|
108
|
+
tally: {
|
|
109
|
+
...defaultMyelophoneConfig.tally,
|
|
110
|
+
...config?.tally,
|
|
111
|
+
},
|
|
112
|
+
blog: {
|
|
113
|
+
...defaultMyelophoneConfig.blog,
|
|
114
|
+
...config?.blog,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const resolveMulti18nConfig = (config: unknown): MyelophoneMulti18nConfig => {
|
|
119
|
+
const value = (config || {}) as Partial<MyelophoneMulti18nConfig>;
|
|
120
|
+
const defaultLocale = value.defaultLocale || 'en';
|
|
121
|
+
const locales = [
|
|
122
|
+
...new Set(value.locales?.length ? value.locales : ['en']),
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
defaultLocale,
|
|
127
|
+
locales,
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const resolveClientConfig = (
|
|
132
|
+
config: Partial<MyelophoneConfig>,
|
|
133
|
+
multi18n: unknown,
|
|
134
|
+
): MyelophoneClientConfig => ({
|
|
135
|
+
...resolveMyelophoneConfig(config),
|
|
136
|
+
multi18n: resolveMulti18nConfig(multi18n),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
export default defineNuxtModule<MyelophoneConfig>({
|
|
140
|
+
meta: {
|
|
141
|
+
name: '@myelophone/nuxt-config',
|
|
142
|
+
configKey: 'myelophone',
|
|
143
|
+
compatibility: { nuxt: '^4.0.0' },
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
defaults: defaultMyelophoneConfig,
|
|
147
|
+
|
|
148
|
+
setup(options, nuxt) {
|
|
149
|
+
const head = nuxt.options.app.head as Record<string, unknown>;
|
|
150
|
+
const staticTitle = head.title;
|
|
151
|
+
if (typeof staticTitle === 'string') {
|
|
152
|
+
const appConfig = nuxt.options.appConfig as Record<string, unknown>;
|
|
153
|
+
const defaultSeo =
|
|
154
|
+
(appConfig.defaultSeo as Record<string, unknown> | undefined) ||
|
|
155
|
+
{};
|
|
156
|
+
nuxt.options.appConfig = {
|
|
157
|
+
...appConfig,
|
|
158
|
+
defaultSeo: {
|
|
159
|
+
...defaultSeo,
|
|
160
|
+
title:
|
|
161
|
+
typeof defaultSeo.title === 'string'
|
|
162
|
+
? defaultSeo.title
|
|
163
|
+
: staticTitle,
|
|
164
|
+
},
|
|
165
|
+
} as typeof nuxt.options.appConfig;
|
|
166
|
+
delete head.title;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const logger = useLogger('@myelophone/nuxt');
|
|
170
|
+
const myelophone = resolveClientConfig(options, nuxt.options.multi18n);
|
|
171
|
+
const isStreamingSSR =
|
|
172
|
+
!isSSG && (isSSRStreaming || myelophone.ssrStream === true);
|
|
173
|
+
|
|
174
|
+
nuxt.options.myelophone = myelophone;
|
|
175
|
+
nuxt.options.runtimeConfig.myelophone = myelophone;
|
|
176
|
+
|
|
177
|
+
setSSRStreaming(nuxt.options.experimental, isStreamingSSR);
|
|
178
|
+
|
|
179
|
+
if (isStreamingSSR) {
|
|
180
|
+
nuxt.options.routeRules ||= {};
|
|
181
|
+
enableDefaultRouteStreaming(nuxt.options.routeRules);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
nuxt.hook('build:before', () => {
|
|
185
|
+
logger.info(
|
|
186
|
+
'Thank You for using @MyelophOne/nuxt. With ❤️ from @aleksivanou (aleksivanov.me)',
|
|
187
|
+
);
|
|
188
|
+
if (isStreamingSSR) {
|
|
189
|
+
logger.info('Building with Nuxt streaming SSR enabled.');
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
nuxt.hook('nitro:config', (nitroConfig) => {
|
|
194
|
+
nitroConfig.runtimeConfig ||= {};
|
|
195
|
+
nitroConfig.runtimeConfig.myelophone = myelophone;
|
|
196
|
+
|
|
197
|
+
if (isStreamingSSR) {
|
|
198
|
+
nitroConfig.routeRules ||= {};
|
|
199
|
+
enableDefaultRouteStreaming(nitroConfig.routeRules);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
nuxt.hook('modules:done', () => {
|
|
204
|
+
const config = nuxt.options.runtimeConfig;
|
|
205
|
+
config.myelophone = myelophone;
|
|
206
|
+
|
|
207
|
+
const keysToRemove = ['nuxt-seo-utils-version'];
|
|
208
|
+
|
|
209
|
+
keysToRemove.forEach((key) => {
|
|
210
|
+
if (config.public && key in config.public) {
|
|
211
|
+
delete config.public[key];
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const objectsToRemoveVersion = ['nuxt-scripts', 'nuxt-robots'];
|
|
216
|
+
|
|
217
|
+
objectsToRemoveVersion.forEach((objKey) => {
|
|
218
|
+
if (
|
|
219
|
+
config.public &&
|
|
220
|
+
objKey in config.public &&
|
|
221
|
+
config.public[objKey]
|
|
222
|
+
) {
|
|
223
|
+
const target = config.public[objKey];
|
|
224
|
+
delete target['version' as keyof typeof target];
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
extendViteConfig((config) => {
|
|
230
|
+
config.build = config.build || {};
|
|
231
|
+
config.build.cssCodeSplit = myelophone.splitCss === true;
|
|
232
|
+
config.define = {
|
|
233
|
+
...config.define,
|
|
234
|
+
__MYELOPHONE_CONFIG__: JSON.stringify(myelophone).replace(
|
|
235
|
+
/</g,
|
|
236
|
+
'\\u003C',
|
|
237
|
+
),
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
},
|
|
241
|
+
});
|