@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.
Files changed (259) hide show
  1. package/.devcontainer/Dockerfile +7 -0
  2. package/.devcontainer/devcontainer.json +21 -0
  3. package/.editorconfig +43 -0
  4. package/.github/workflows/package.yml +73 -0
  5. package/.nuxtignore +6 -0
  6. package/.releaserc.js +132 -0
  7. package/.yarn/releases/yarn-4.18.0.cjs +1000 -0
  8. package/.yarnrc.yml +8 -0
  9. package/CHANGELOG.md +141 -0
  10. package/LICENSE +77 -0
  11. package/README.md +3004 -0
  12. package/app/app.vue +455 -0
  13. package/app/assets/css/animation.css +81 -0
  14. package/app/assets/css/backgrounds.css +57 -0
  15. package/app/assets/css/base.css +182 -0
  16. package/app/assets/css/index.css +9 -0
  17. package/app/assets/css/links.css +12 -0
  18. package/app/assets/css/multimedia.css +102 -0
  19. package/app/assets/css/preloader.css +68 -0
  20. package/app/assets/css/print.css +40 -0
  21. package/app/assets/css/tailwind.css +20 -0
  22. package/app/assets/css/theme.css +29 -0
  23. package/app/components/consent/GoogleMap.vue +59 -0
  24. package/app/components/consent/Youtube.vue +27 -0
  25. package/app/components/cookie/Banner.vue +70 -0
  26. package/app/components/cookie/ConsentWrapper.vue +71 -0
  27. package/app/components/cookie/PrivacyPolicy.vue +179 -0
  28. package/app/components/cookie/SettingsModal.vue +387 -0
  29. package/app/components/grid/Col.vue +262 -0
  30. package/app/components/grid/Container.vue +236 -0
  31. package/app/components/grid/Row.vue +248 -0
  32. package/app/components/grid/Stack.vue +233 -0
  33. package/app/components/myelophone/Copyright.vue +5 -0
  34. package/app/components/myelophone/Welcome.vue +107 -0
  35. package/app/components/page/FullscreenPreloader.vue +177 -0
  36. package/app/components/page/Preloader.vue +18 -0
  37. package/app/components/page/ReadyLocalized.vue +19 -0
  38. package/app/components/ready/.gitkeep +0 -0
  39. package/app/components/safe/Email.vue +52 -0
  40. package/app/components/safe/ImgWithLoader.vue +180 -0
  41. package/app/components/safe/NuxtImg.vue +25 -0
  42. package/app/components/safe/NuxtPicture.vue +29 -0
  43. package/app/components/section/.gitkeep +0 -0
  44. package/app/components/seo/BrandInfo.vue +76 -0
  45. package/app/components/seo/ContentNoIndex.vue +35 -0
  46. package/app/components/seo/NoIndex.vue +22 -0
  47. package/app/components/seo/OrgInfo.vue +130 -0
  48. package/app/components/seo/ProfileInfo.vue +137 -0
  49. package/app/components/ui/Accordion.vue +163 -0
  50. package/app/components/ui/Alert.vue +179 -0
  51. package/app/components/ui/Avatar.vue +88 -0
  52. package/app/components/ui/Badge.vue +150 -0
  53. package/app/components/ui/Banner.vue +133 -0
  54. package/app/components/ui/Button.vue +175 -0
  55. package/app/components/ui/Card.vue +66 -0
  56. package/app/components/ui/Checkbox.vue +134 -0
  57. package/app/components/ui/Chip.vue +136 -0
  58. package/app/components/ui/CommandPalette.vue +434 -0
  59. package/app/components/ui/Console.vue +583 -0
  60. package/app/components/ui/CursorCreative.vue +148 -0
  61. package/app/components/ui/FeatureAccordion.vue +165 -0
  62. package/app/components/ui/FileUpload.vue +705 -0
  63. package/app/components/ui/GeoDependent.vue +72 -0
  64. package/app/components/ui/Heading.vue +51 -0
  65. package/app/components/ui/Icon.vue +42 -0
  66. package/app/components/ui/Icon8.vue +39 -0
  67. package/app/components/ui/Input.vue +88 -0
  68. package/app/components/ui/LangLink.vue +34 -0
  69. package/app/components/ui/LangVisible.vue +33 -0
  70. package/app/components/ui/LanguageSelect.vue +202 -0
  71. package/app/components/ui/LightBox.vue +155 -0
  72. package/app/components/ui/LocalTime.vue +170 -0
  73. package/app/components/ui/Modal.vue +161 -0
  74. package/app/components/ui/Popup.vue +200 -0
  75. package/app/components/ui/PostContent.server.vue +23 -0
  76. package/app/components/ui/ProtectedEmail.vue +52 -0
  77. package/app/components/ui/RelativeTime.vue +141 -0
  78. package/app/components/ui/ScheduledContent.vue +150 -0
  79. package/app/components/ui/ScrollToTop.client.vue +48 -0
  80. package/app/components/ui/SegmentedControl.vue +148 -0
  81. package/app/components/ui/SiteSearch.vue +1191 -0
  82. package/app/components/ui/Skeleton.vue +91 -0
  83. package/app/components/ui/SmartContrast.vue +74 -0
  84. package/app/components/ui/SnapContainer.vue +222 -0
  85. package/app/components/ui/SnapSection.vue +294 -0
  86. package/app/components/ui/SonnerStyle.vue +7 -0
  87. package/app/components/ui/SplitSection.vue +99 -0
  88. package/app/components/ui/Steps.vue +858 -0
  89. package/app/components/ui/StickyWrapper.vue +46 -0
  90. package/app/components/ui/Table.vue +251 -0
  91. package/app/components/ui/Tabs.vue +451 -0
  92. package/app/components/ui/TextColumn.vue +36 -0
  93. package/app/components/ui/Textarea.vue +251 -0
  94. package/app/components/ui/Toggler.vue +40 -0
  95. package/app/components/ui/Tooltip.vue +524 -0
  96. package/app/components/ui/TruncateText.vue +105 -0
  97. package/app/components/ui/ViewportSpacer.vue +72 -0
  98. package/app/components/view/AnimatedCounter.vue +121 -0
  99. package/app/components/view/AnnouncementBar.vue +104 -0
  100. package/app/components/view/AvatarGroup.vue +104 -0
  101. package/app/components/view/Breadcrumbs.vue +97 -0
  102. package/app/components/view/ContentWithToc.vue +152 -0
  103. package/app/components/view/Copyright.vue +68 -0
  104. package/app/components/view/CountdownTimer.vue +184 -0
  105. package/app/components/view/CreativeCTA.vue +143 -0
  106. package/app/components/view/CurrencySelect.vue +254 -0
  107. package/app/components/view/FeatureBoxGrid.vue +142 -0
  108. package/app/components/view/HorizontalMenu.vue +97 -0
  109. package/app/components/view/ImageCompare.vue +141 -0
  110. package/app/components/view/ImgSlider.vue +333 -0
  111. package/app/components/view/InfiniteMarquee.vue +147 -0
  112. package/app/components/view/InfoBar.vue +86 -0
  113. package/app/components/view/LogoGrid.vue +106 -0
  114. package/app/components/view/LogoSlider.vue +83 -0
  115. package/app/components/view/NotFound.vue +81 -0
  116. package/app/components/view/PartialBackground.vue +313 -0
  117. package/app/components/view/QuoteBig.vue +111 -0
  118. package/app/components/view/ResponsiveMenu.vue +219 -0
  119. package/app/components/view/ReviewsSlider.vue +230 -0
  120. package/app/components/view/SocialBar.vue +78 -0
  121. package/app/components/view/StickyHeadingContent.vue +158 -0
  122. package/app/components/view/TallyForm.vue +295 -0
  123. package/app/components/view/WarningOutdated.vue +113 -0
  124. package/app/composables/useApi.ts +88 -0
  125. package/app/composables/useAppSeo.ts +78 -0
  126. package/app/composables/useBreadcrumbs.ts +106 -0
  127. package/app/composables/useChangeUrl.ts +73 -0
  128. package/app/composables/useCommand.ts +28 -0
  129. package/app/composables/useCookieControl.ts +111 -0
  130. package/app/composables/useCookieModal.ts +9 -0
  131. package/app/composables/useDevice.ts +109 -0
  132. package/app/composables/useGeoFetch.ts +139 -0
  133. package/app/composables/useLoadScript.ts +36 -0
  134. package/app/composables/useModalState.ts +37 -0
  135. package/app/composables/useMultiLang.ts +278 -0
  136. package/app/composables/useNetwork.ts +49 -0
  137. package/app/composables/useNuxtVersion.ts +5 -0
  138. package/app/composables/usePiniaStore.ts +5 -0
  139. package/app/composables/usePopupControl.ts +57 -0
  140. package/app/composables/useQuery.ts +307 -0
  141. package/app/composables/useReadyPage.ts +182 -0
  142. package/app/composables/useSafeList.ts +90 -0
  143. package/app/composables/useScriptLoader.ts +221 -0
  144. package/app/composables/useSiteSearch.ts +32 -0
  145. package/app/composables/useSnapScroll.ts +24 -0
  146. package/app/composables/useStatic.ts +5 -0
  147. package/app/composables/useStorage.ts +252 -0
  148. package/app/composables/useStoreBroadcast.ts +146 -0
  149. package/app/composables/useThemeSync.ts +81 -0
  150. package/app/constants/layout.ts +109 -0
  151. package/app/constants/predefinedCookieScripts.ts +1222 -0
  152. package/app/constants/ui-events.ts +1 -0
  153. package/app/error.vue +169 -0
  154. package/app/layouts/default-blog.vue +5 -0
  155. package/app/layouts/default.vue +3 -0
  156. package/app/locales/en/commands.json +5 -0
  157. package/app/locales/en/cookies.json +182 -0
  158. package/app/locales/en/error.json +4 -0
  159. package/app/locales/en/interface.json +33 -0
  160. package/app/locales/en/myelophone.json +8 -0
  161. package/app/locales/en/outdated.json +5 -0
  162. package/app/locales/en/times.json +18 -0
  163. package/app/locales/pl/commands.json +5 -0
  164. package/app/locales/pl/cookies.json +182 -0
  165. package/app/locales/pl/error.json +4 -0
  166. package/app/locales/pl/interface.json +33 -0
  167. package/app/locales/pl/myelophone.json +8 -0
  168. package/app/locales/pl/outdated.json +5 -0
  169. package/app/locales/pl/times.json +18 -0
  170. package/app/locales/ru/commands.json +5 -0
  171. package/app/locales/ru/cookies.json +182 -0
  172. package/app/locales/ru/error.json +4 -0
  173. package/app/locales/ru/interface.json +33 -0
  174. package/app/locales/ru/myelophone.json +8 -0
  175. package/app/locales/ru/outdated.json +5 -0
  176. package/app/locales/ru/times.json +18 -0
  177. package/app/middleware/blog-layout.ts +28 -0
  178. package/app/middleware/i18n.global.ts +17 -0
  179. package/app/middleware/not-found-status.ts +11 -0
  180. package/app/modules/default-blog/index.ts +39 -0
  181. package/app/modules/default-blog/posts.ts +80 -0
  182. package/app/modules/default-sitemap/index.ts +116 -0
  183. package/app/modules/default-sitemap/runtime/sitemap.get.ts +42 -0
  184. package/app/modules/fullscreen-preloader/module.ts +43 -0
  185. package/app/modules/htaccess/.htaccess +38 -0
  186. package/app/modules/htaccess/index.ts +23 -0
  187. package/app/modules/i18n-shaker.mjs +221 -0
  188. package/app/modules/multi18n/module.ts +71 -0
  189. package/app/modules/myelophone-config/module.ts +241 -0
  190. package/app/pages/[...all].vue +9 -0
  191. package/app/pages/index.vue +3 -0
  192. package/app/pages/post/[...slug].vue +30 -0
  193. package/app/plugins/bfcache-refresh.client.ts +9 -0
  194. package/app/plugins/canonical.ts +30 -0
  195. package/app/plugins/csp-nonce.server.ts +41 -0
  196. package/app/plugins/multi18n.client.ts +21 -0
  197. package/app/plugins/multi18n.server.ts +15 -0
  198. package/app/plugins/nuxt-version.client.ts +4 -0
  199. package/app/plugins/restore.client.ts +21 -0
  200. package/app/plugins/reveal.ts +89 -0
  201. package/app/plugins/url-helper.client.ts +66 -0
  202. package/app/server/api/exchange-rates.get.ts +216 -0
  203. package/app/server/api/healthz.ts +1 -0
  204. package/app/server/api/logout.post.ts +6 -0
  205. package/app/server/middleware/normalize-url.ts +14 -0
  206. package/app/server/middleware/security-headers.ts +147 -0
  207. package/app/server/middleware/url-helper.ts +43 -0
  208. package/app/server/plugins/clean-html-spaces.ts +15 -0
  209. package/app/server/plugins/clean-styles.ts +94 -0
  210. package/app/server/plugins/compression.ts +122 -0
  211. package/app/server/plugins/css-preload.ts +26 -0
  212. package/app/server/plugins/early-hints.ts +31 -0
  213. package/app/server/plugins/force-noindex.ts +43 -0
  214. package/app/server/plugins/html-one-line.ts +17 -0
  215. package/app/server/plugins/inline-style.ts +7 -0
  216. package/app/server/routes/healthz.get.ts +1 -0
  217. package/app/server/tsconfig.json +3 -0
  218. package/app/stores/cart.ts +945 -0
  219. package/app/stores/commands.ts +43 -0
  220. package/app/stores/device.ts +156 -0
  221. package/app/stores/querySync.ts +5 -0
  222. package/app/stores/settings.ts +145 -0
  223. package/app/stores/user.ts +607 -0
  224. package/app/types/cookie.ts +89 -0
  225. package/app/types/myelophone.d.ts +87 -0
  226. package/app/types/site-search.ts +110 -0
  227. package/app/utils/asyncRefresh.ts +4 -0
  228. package/app/utils/blog.server.ts +142 -0
  229. package/app/utils/jsonLd.ts +56 -0
  230. package/app/utils/myelophoneConfig.ts +4 -0
  231. package/app/utils/sortBy.ts +18 -0
  232. package/app/utils/transliterate.ts +75 -0
  233. package/app/utils/ytVideo.ts +37 -0
  234. package/app/workers/site-search.worker.ts +661 -0
  235. package/biome.json +94 -0
  236. package/commitlint.config.js +3 -0
  237. package/nuxt.config.ts +539 -0
  238. package/package.json +101 -0
  239. package/playground/content/.gitkeep +0 -0
  240. package/playground/locales/.gitkeep +0 -0
  241. package/playground/myelophone.dev.ts +45 -0
  242. package/playground/nuxt.config.ts +105 -0
  243. package/playground/package.json +5 -0
  244. package/playground/pages/.gitkeep +0 -0
  245. package/playground/tsconfig.json +3 -0
  246. package/playwright.config.ts +23 -0
  247. package/public/browsers/chrome.svg +77 -0
  248. package/public/browsers/edge.svg +98 -0
  249. package/public/browsers/firefox.svg +300 -0
  250. package/public/favicon.ico +0 -0
  251. package/public/myelophone_eng.png +0 -0
  252. package/public/myelophone_eng_white.png +0 -0
  253. package/spa-loading-template.html +72 -0
  254. package/tests/home.spec.ts +9 -0
  255. package/tests/utils.ts +23 -0
  256. package/tsconfig.json +16 -0
  257. package/vite/plugins/clean-css.ts +190 -0
  258. package/vite/plugins/postcss-add-viewportunits.ts +33 -0
  259. 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, '&amp;')
38
+ .replace(/</g, '&lt;')
39
+ .replace(/>/g, '&gt;')
40
+ .replace(/"/g, '&quot;')
41
+ .replace(/'/g, '&apos;');
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
+ });
@@ -0,0 +1,9 @@
1
+ <script setup lang="ts">
2
+ definePageMeta({
3
+ middleware: "not-found-status",
4
+ });
5
+ </script>
6
+
7
+ <template>
8
+ <ViewNotFound />
9
+ </template>
@@ -0,0 +1,3 @@
1
+ <template>
2
+ <MyelophoneWelcome />
3
+ </template>