@turnipxenon/pineapple 4.5.0 → 4.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Server middleware that handles locale-based routing and request processing.
3
+ *
4
+ * This middleware performs several key functions:
5
+ *
6
+ * 1. Determines the locale for the incoming request using configured strategies
7
+ * 2. Handles URL localization and redirects (only for document requests)
8
+ * 3. Maintains locale state using AsyncLocalStorage to prevent request interference
9
+ *
10
+ * When URL strategy is used:
11
+ *
12
+ * - The locale is extracted from the URL for all request types
13
+ * - If URL doesn't match the determined locale, redirects to localized URL (only for document requests)
14
+ * - De-localizes URLs before passing to server (e.g., `/fr/about` → `/about`)
15
+ *
16
+ * @template T - The return type of the resolve function
17
+ *
18
+ * @param {Request} request - The incoming request object
19
+ * @param {(args: { request: Request, locale: import("./runtime.js").Locale }) => T | Promise<T>} resolve - Function to handle the request
20
+ *
21
+ * @returns {Promise<Response>}
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * // Basic usage in metaframeworks like NextJS, SvelteKit, Astro, Nuxt, etc.
26
+ * export const handle = async ({ event, resolve }) => {
27
+ * return serverMiddleware(event.request, ({ request, locale }) => {
28
+ * // let the framework further resolve the request
29
+ * return resolve(request);
30
+ * });
31
+ * };
32
+ * ```
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * // Usage in a framework like Express JS or Hono
37
+ * app.use(async (req, res, next) => {
38
+ * const result = await serverMiddleware(req, ({ request, locale }) => {
39
+ * // If a redirect happens this won't be called
40
+ * return next(request);
41
+ * });
42
+ * });
43
+ * ```
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * // Usage in serverless environments like Cloudflare Workers
48
+ * // ⚠️ WARNING: This should ONLY be used in serverless environments like Cloudflare Workers.
49
+ * // Disabling AsyncLocalStorage in traditional server environments risks cross-request pollution where state from
50
+ * // one request could leak into another concurrent request.
51
+ * export default {
52
+ * fetch: async (request) => {
53
+ * return serverMiddleware(
54
+ * request,
55
+ * ({ request, locale }) => handleRequest(request, locale),
56
+ * { disableAsyncLocalStorage: true }
57
+ * );
58
+ * }
59
+ * };
60
+ * ```
61
+ */
62
+ export function paraglideMiddleware<T>(request: Request, resolve: (args: {
63
+ request: Request;
64
+ locale: import("./runtime.js").Locale;
65
+ }) => T | Promise<T>): Promise<Response>;
@@ -0,0 +1,174 @@
1
+ // eslint-disable
2
+
3
+ import * as runtime from "./runtime.js";
4
+
5
+ /**
6
+ * Server middleware that handles locale-based routing and request processing.
7
+ *
8
+ * This middleware performs several key functions:
9
+ *
10
+ * 1. Determines the locale for the incoming request using configured strategies
11
+ * 2. Handles URL localization and redirects (only for document requests)
12
+ * 3. Maintains locale state using AsyncLocalStorage to prevent request interference
13
+ *
14
+ * When URL strategy is used:
15
+ *
16
+ * - The locale is extracted from the URL for all request types
17
+ * - If URL doesn't match the determined locale, redirects to localized URL (only for document requests)
18
+ * - De-localizes URLs before passing to server (e.g., `/fr/about` → `/about`)
19
+ *
20
+ * @template T - The return type of the resolve function
21
+ *
22
+ * @param {Request} request - The incoming request object
23
+ * @param {(args: { request: Request, locale: import("./runtime.js").Locale }) => T | Promise<T>} resolve - Function to handle the request
24
+ *
25
+ * @returns {Promise<Response>}
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * // Basic usage in metaframeworks like NextJS, SvelteKit, Astro, Nuxt, etc.
30
+ * export const handle = async ({ event, resolve }) => {
31
+ * return serverMiddleware(event.request, ({ request, locale }) => {
32
+ * // let the framework further resolve the request
33
+ * return resolve(request);
34
+ * });
35
+ * };
36
+ * ```
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * // Usage in a framework like Express JS or Hono
41
+ * app.use(async (req, res, next) => {
42
+ * const result = await serverMiddleware(req, ({ request, locale }) => {
43
+ * // If a redirect happens this won't be called
44
+ * return next(request);
45
+ * });
46
+ * });
47
+ * ```
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * // Usage in serverless environments like Cloudflare Workers
52
+ * // ⚠️ WARNING: This should ONLY be used in serverless environments like Cloudflare Workers.
53
+ * // Disabling AsyncLocalStorage in traditional server environments risks cross-request pollution where state from
54
+ * // one request could leak into another concurrent request.
55
+ * export default {
56
+ * fetch: async (request) => {
57
+ * return serverMiddleware(
58
+ * request,
59
+ * ({ request, locale }) => handleRequest(request, locale),
60
+ * { disableAsyncLocalStorage: true }
61
+ * );
62
+ * }
63
+ * };
64
+ * ```
65
+ */
66
+ export async function paraglideMiddleware(request, resolve) {
67
+ if (!runtime.disableAsyncLocalStorage && !runtime.serverAsyncLocalStorage) {
68
+ const { AsyncLocalStorage } = await import("async_hooks");
69
+ runtime.overwriteServerAsyncLocalStorage(new AsyncLocalStorage());
70
+ }
71
+ else if (!runtime.serverAsyncLocalStorage) {
72
+ runtime.overwriteServerAsyncLocalStorage(createMockAsyncLocalStorage());
73
+ }
74
+ const locale = runtime.extractLocaleFromRequest(request);
75
+ const origin = new URL(request.url).origin;
76
+ // if the client makes a request to a URL that doesn't match
77
+ // the localizedUrl, redirect the client to the localized URL
78
+ if (request.headers.get("Sec-Fetch-Dest") === "document" &&
79
+ runtime.strategy.includes("url")) {
80
+ const localizedUrl = runtime.localizeUrl(request.url, { locale });
81
+ if (normalizeURL(localizedUrl.href) !== normalizeURL(request.url)) {
82
+ return Response.redirect(localizedUrl, 307);
83
+ }
84
+ }
85
+ // If the strategy includes "url", we need to de-localize the URL
86
+ // before passing it to the server middleware.
87
+ //
88
+ // The middleware is responsible for mapping a localized URL to the
89
+ // de-localized URL e.g. `/en/about` to `/about`. Otherwise,
90
+ // the server can't render the correct page.
91
+ const newRequest = runtime.strategy.includes("url")
92
+ ? new Request(runtime.deLocalizeUrl(request.url), request)
93
+ : // need to create a new request object because some metaframeworks (nextjs!) throw otherwise
94
+ // https://github.com/opral/inlang-paraglide-js/issues/411
95
+ new Request(request);
96
+ // the message functions that have been called in this request
97
+ /** @type {Set<string>} */
98
+ const messageCalls = new Set();
99
+ const response = await runtime.serverAsyncLocalStorage?.run({ locale, origin, messageCalls }, () => resolve({ locale, request: newRequest }));
100
+ // Only modify HTML responses
101
+ if (runtime.experimentalMiddlewareLocaleSplitting &&
102
+ response.headers.get("Content-Type")?.includes("html")) {
103
+ const body = await response.text();
104
+ const messages = [];
105
+ // using .values() to avoid polyfilling in older projects. else the following error is thrown
106
+ // Type 'Set<string>' can only be iterated through when using the '--downlevelIteration' flag or with a '--target' of 'es2015' or higher.
107
+ for (const messageCall of Array.from(messageCalls)) {
108
+ const [id, locale] =
109
+ /** @type {[string, import("./runtime.js").Locale]} */ (messageCall.split(":"));
110
+ messages.push(`${id}: ${compiledBundles[id]?.[locale]}`);
111
+ }
112
+ const script = `<script>globalThis.__paraglide_ssr = { ${messages.join(",")} }</script>`;
113
+ // Insert the script before the closing head tag
114
+ const newBody = body.replace("</head>", `${script}</head>`);
115
+ // Create a new response with the modified body
116
+ // Clone all headers except Content-Length which will be set automatically
117
+ const newHeaders = new Headers(response.headers);
118
+ newHeaders.delete("Content-Length"); // Let the browser calculate the correct length
119
+ return new Response(newBody, {
120
+ status: response.status,
121
+ statusText: response.statusText,
122
+ headers: newHeaders,
123
+ });
124
+ }
125
+ return response;
126
+ }
127
+ /**
128
+ * Normalize url for comparison.
129
+ * Strips trailing slash
130
+ * @param {string} url
131
+ * @returns {string} normalized url string
132
+ */
133
+ function normalizeURL(url) {
134
+ const urlObj = new URL(url);
135
+ // // strip trailing slash from pathname
136
+ urlObj.pathname = urlObj.pathname.replace(/\/$/, "");
137
+ return urlObj.href;
138
+ }
139
+ /**
140
+ * Creates a mock AsyncLocalStorage implementation for environments where
141
+ * native AsyncLocalStorage is not available or disabled.
142
+ *
143
+ * This mock implementation mimics the behavior of the native AsyncLocalStorage
144
+ * but doesn't require the async_hooks module. It's designed to be used in
145
+ * environments like Cloudflare Workers where AsyncLocalStorage is not available.
146
+ *
147
+ * @returns {import("./runtime.js").ParaglideAsyncLocalStorage}
148
+ */
149
+ function createMockAsyncLocalStorage() {
150
+ /** @type {any} */
151
+ let currentStore = undefined;
152
+ return {
153
+ getStore() {
154
+ return currentStore;
155
+ },
156
+ async run(store, callback) {
157
+ currentStore = store;
158
+ try {
159
+ return await callback();
160
+ }
161
+ finally {
162
+ currentStore = undefined;
163
+ }
164
+ },
165
+ };
166
+ }
167
+ /**
168
+ * The compiled messages for the server middleware.
169
+ *
170
+ * Only populated if `enableMiddlewareOptimizations` is set to `true`.
171
+ *
172
+ * @type {Record<string, Record<import("./runtime.js").Locale, string>>}
173
+ */
174
+ const compiledBundles = {};
@@ -1,10 +1,9 @@
1
1
  <script lang="ts" generics="T extends string">
2
- import { Combobox } from '@skeletonlabs/skeleton-svelte';
3
- import { getLocale, localizeHref } from '../../../paraglide/runtime';
4
2
  import type { PinyaComboboxProps, ValueChangeDetails } from "./PinyaComboboxProps";
3
+ import { Combobox } from "@skeletonlabs/skeleton-svelte";
5
4
 
6
5
  let {
7
- contentZIndex = 'auto',
6
+ contentZIndex = "auto",
8
7
  value = $bindable(),
9
8
  onValueChange = () => {},
10
9
  onValueChangeBase = undefined,
@@ -32,4 +31,4 @@
32
31
  onValueChange={onValueChangeBase ?? onValueChangeBaseImpl}
33
32
  {value}
34
33
  {...props}
35
- />
34
+ />
@@ -8,7 +8,7 @@
8
8
  type ParsePageMetaCompareFn
9
9
  } from "./PageMeta";
10
10
  import { PinyaCard } from "../../elements/index";
11
- import { localizeHref } from "../../../paraglide/runtime.js";
11
+ import { localizeHref } from "../../../external/paraglide/runtime.js";
12
12
 
13
13
  interface Props {
14
14
  fileList: Record<string, unknown>;
@@ -2,7 +2,7 @@
2
2
  import type { ModalProps } from 'svelte-modals';
3
3
  import { setMode, userPrefersMode } from 'mode-watcher';
4
4
 
5
- import { m } from '../../../../paraglide/messages';
5
+ import { m } from '../../../../external/paraglide/messages';
6
6
  import ModalBase from '../../../components/ModalBase.svelte';
7
7
  import DarkIcon from '../../../../assets/icons/icon-dark-mode.svg';
8
8
  import LightIcon from '../../../../assets/icons/icon-light-mode.svg';
@@ -107,4 +107,4 @@
107
107
  text-align: start;
108
108
  gap: 1lh;
109
109
  }
110
- </style>
110
+ </style>
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { deLocalizeHref, getLocale, localizeHref } from "../../../../paraglide/runtime";
2
+ import { deLocalizeHref, getLocale, localizeHref } from "../../../../external/paraglide/runtime";
3
3
  import PinyaCombobox from "../../../elements/pinya-combobox/PinyaCombobox.svelte";
4
4
  import { appState } from "../../../templates/PinyaPageLayout/runes.svelte";
5
5
 
@@ -40,4 +40,4 @@
40
40
  placeholder="Select Language"
41
41
  {onValueChange}
42
42
  {disabled}
43
- />
43
+ />
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { page } from "$app/state";
3
- import { locales, localizeHref } from "../../paraglide/runtime";
3
+ import { locales, localizeHref } from "../../external/paraglide/runtime";
4
4
  import "../../styles/global.css";
5
5
  import WebThumbnailImage from "../../assets/placeholder/placeholder_circle.png";
6
6
  import type { PinyaHead } from "./runes.svelte";
@@ -1,8 +1,8 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from "svelte";
3
3
  import { modals } from "svelte-modals";
4
- import { m } from "../../../paraglide/messages";
5
- import { localizeHref } from "../../../paraglide/runtime";
4
+ import { m } from "../../../external/paraglide/messages";
5
+ import { localizeHref } from "../../../external/paraglide/runtime";
6
6
 
7
7
  import AresLogo from "../../../assets/characters/ares/ares_logo.webp";
8
8
  import SettingsLogo from "../../../assets/icons/icon-settings.svg";
@@ -121,4 +121,4 @@
121
121
  width: 100%;
122
122
  height: calc(var(--dialog-box-height) + 3.5lh);
123
123
  }
124
- </style>
124
+ </style>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@turnipxenon/pineapple",
3
3
  "description": "personal package for base styling for other personal projects",
4
- "version": "4.5.0",
4
+ "version": "4.5.1",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
7
7
  "build": "vite build",