@decocms/blocks 7.3.0 → 7.4.0
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/package.json +2 -1
- package/src/cms/sectionMixins.ts +1 -1
- package/src/hooks/Picture.tsx +25 -2
- package/src/sdk/detectDevice.ts +45 -0
- package/src/sdk/env.test.ts +33 -0
- package/src/sdk/env.ts +29 -6
- package/src/sdk/requestContext.ts +1 -1
- package/src/sdk/useDevice.ts +12 -32
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/blocks",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco framework-agnostic core: CMS block resolution, section registry, matchers, portable SDK utilities",
|
|
6
6
|
"repository": {
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"./sdk/cookie": "./src/sdk/cookie.ts",
|
|
47
47
|
"./sdk/crypto": "./src/sdk/crypto.ts",
|
|
48
48
|
"./sdk/csp": "./src/sdk/csp.ts",
|
|
49
|
+
"./sdk/detectDevice": "./src/sdk/detectDevice.ts",
|
|
49
50
|
"./sdk/djb2": "./src/sdk/djb2.ts",
|
|
50
51
|
"./sdk/encoding": "./src/sdk/encoding.ts",
|
|
51
52
|
"./sdk/env": "./src/sdk/env.ts",
|
package/src/cms/sectionMixins.ts
CHANGED
package/src/hooks/Picture.tsx
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// `"use client"` added by @decocms/nextjs's next-smoke fixture build
|
|
4
|
+
// (Picture is reachable from `DecoRootLayout`/`SectionRenderer` via the
|
|
5
|
+
// `hooks/index.ts` barrel, so it is part of the react-server module graph
|
|
6
|
+
// on any Next.js Server Component page even when a given page never
|
|
7
|
+
// renders it): `createContext`/`useContext` are unavailable under React's
|
|
8
|
+
// react-server condition, and Next statically flags any reachable module
|
|
9
|
+
// that imports them without this directive ("You're importing a component
|
|
10
|
+
// that needs createContext...") — same class of fix, same precedent, as
|
|
11
|
+
// `SectionErrorFallback.tsx`'s `"use client"` (class component /
|
|
12
|
+
// componentDidCatch, also client-only).
|
|
1
13
|
import {
|
|
2
14
|
type ComponentPropsWithoutRef,
|
|
15
|
+
type Context,
|
|
3
16
|
createContext,
|
|
4
17
|
forwardRef,
|
|
5
18
|
type ReactNode,
|
|
@@ -12,13 +25,22 @@ import { type FitOptions, getOptimizedMediaUrl, getSrcSet } from "./Image";
|
|
|
12
25
|
// Preload context — flows from <Picture preload> to child <Source> elements
|
|
13
26
|
// so each source can inject its own <link rel="preload"> with the correct
|
|
14
27
|
// media query for responsive art direction.
|
|
28
|
+
//
|
|
29
|
+
// Still created LAZILY (first render, not module scope) even under
|
|
30
|
+
// `"use client"`: this keeps a single memoized context object regardless of
|
|
31
|
+
// how many client bundle chunks end up importing this module, rather than
|
|
32
|
+
// relying on module-instance identity across chunks.
|
|
15
33
|
// -------------------------------------------------------------------------
|
|
16
34
|
|
|
17
35
|
interface PreloadContextValue {
|
|
18
36
|
preload: boolean;
|
|
19
37
|
}
|
|
20
38
|
|
|
21
|
-
|
|
39
|
+
let _preloadContext: Context<PreloadContextValue> | null = null;
|
|
40
|
+
function getPreloadContext(): Context<PreloadContextValue> {
|
|
41
|
+
_preloadContext ??= createContext<PreloadContextValue>({ preload: false });
|
|
42
|
+
return _preloadContext;
|
|
43
|
+
}
|
|
22
44
|
|
|
23
45
|
// -------------------------------------------------------------------------
|
|
24
46
|
// Source — composable <source> with automatic srcSet optimization and
|
|
@@ -41,7 +63,7 @@ export const Source = forwardRef<HTMLSourceElement, SourceProps>(function Source
|
|
|
41
63
|
{ src, width, height, fetchPriority, fit = "cover", ...rest },
|
|
42
64
|
ref,
|
|
43
65
|
) {
|
|
44
|
-
const { preload } = useContext(
|
|
66
|
+
const { preload } = useContext(getPreloadContext());
|
|
45
67
|
|
|
46
68
|
const optimizedSrc = getOptimizedMediaUrl({
|
|
47
69
|
originalSrc: src,
|
|
@@ -93,6 +115,7 @@ export const Picture = forwardRef<HTMLPictureElement, PictureProps>(function Pic
|
|
|
93
115
|
ref,
|
|
94
116
|
) {
|
|
95
117
|
const value = useMemo(() => ({ preload }), [preload]);
|
|
118
|
+
const PreloadContext = getPreloadContext();
|
|
96
119
|
|
|
97
120
|
return (
|
|
98
121
|
<PreloadContext.Provider value={value}>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure User-Agent device detection — no React, no RequestContext, no
|
|
3
|
+
* side effects. Import from HERE (not `./useDevice`) in any module that can
|
|
4
|
+
* end up in a Next.js App Router **route handler** graph.
|
|
5
|
+
*
|
|
6
|
+
* Why this file exists: `useDevice.ts` re-exports the `"use client"`
|
|
7
|
+
* context half (`./useDeviceContext`, which calls `createContext` at module
|
|
8
|
+
* scope) for backward compatibility. That re-export is harmless in Server
|
|
9
|
+
* Components — Next honors the `"use client"` boundary there — but route
|
|
10
|
+
* handlers (App Router route.ts files) have NO client graph: the directive
|
|
11
|
+
* is ignored, the module executes against Next's vendored **RSC** React
|
|
12
|
+
* (`vendored/rsc/react.js`), and that build has no `createContext`, so the
|
|
13
|
+
* whole route module crashes at evaluation time
|
|
14
|
+
* ("...createContext is not a function"). `@decocms/nextjs`'s admin route
|
|
15
|
+
* handlers reach device detection via `cms/sectionMixins.ts`
|
|
16
|
+
* (`detectDevice`) and `sdk/requestContext.ts` (`isMobileUA`) — both now
|
|
17
|
+
* import this leaf so their graphs never touch the client context file.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export type Device = "mobile" | "tablet" | "desktop";
|
|
21
|
+
|
|
22
|
+
// Android phones include "Mobile" in their UA; Android tablets do not.
|
|
23
|
+
// Check TABLET_RE first so `android(?!.*mobile)` captures tablets before
|
|
24
|
+
// the MOBILE_RE `android.*mobile` branch matches phones.
|
|
25
|
+
export const MOBILE_RE = /mobile|android.*mobile|iphone|ipod|webos|blackberry|opera mini|iemobile/i;
|
|
26
|
+
export const TABLET_RE = /ipad|tablet|kindle|silk|playbook|android(?!.*mobile)/i;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Simple mobile-or-not check (mobile + tablet = true).
|
|
30
|
+
* Use this for cache key splitting or any context where you
|
|
31
|
+
* only need a mobile/desktop binary decision.
|
|
32
|
+
*/
|
|
33
|
+
export function isMobileUA(userAgent: string): boolean {
|
|
34
|
+
return MOBILE_RE.test(userAgent) || TABLET_RE.test(userAgent);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Detect device type from a User-Agent string.
|
|
39
|
+
* Pure function — no side effects, works anywhere.
|
|
40
|
+
*/
|
|
41
|
+
export function detectDevice(userAgent: string): Device {
|
|
42
|
+
if (TABLET_RE.test(userAgent)) return "tablet";
|
|
43
|
+
if (MOBILE_RE.test(userAgent)) return "mobile";
|
|
44
|
+
return "desktop";
|
|
45
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
// isDevMode() memoises its result in module-level state after the first
|
|
4
|
+
// call, so each case below resets modules and re-imports fresh to get an
|
|
5
|
+
// unmemoised read for the NODE_ENV value it's stubbing.
|
|
6
|
+
describe("isDevMode", () => {
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
vi.unstubAllEnvs();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("is false when NODE_ENV is production", async () => {
|
|
12
|
+
vi.stubEnv("NODE_ENV", "production");
|
|
13
|
+
vi.stubEnv("DECO_PREVIEW", "");
|
|
14
|
+
vi.resetModules();
|
|
15
|
+
const { isDevMode } = await import("./env");
|
|
16
|
+
expect(isDevMode()).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("is true when NODE_ENV is development", async () => {
|
|
20
|
+
vi.stubEnv("NODE_ENV", "development");
|
|
21
|
+
vi.resetModules();
|
|
22
|
+
const { isDevMode } = await import("./env");
|
|
23
|
+
expect(isDevMode()).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("is true when DECO_PREVIEW=true even if NODE_ENV is production", async () => {
|
|
27
|
+
vi.stubEnv("NODE_ENV", "production");
|
|
28
|
+
vi.stubEnv("DECO_PREVIEW", "true");
|
|
29
|
+
vi.resetModules();
|
|
30
|
+
const { isDevMode } = await import("./env");
|
|
31
|
+
expect(isDevMode()).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
package/src/sdk/env.ts
CHANGED
|
@@ -7,12 +7,37 @@
|
|
|
7
7
|
|
|
8
8
|
let _isDev: boolean | null = null;
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Reads `process.env.NODE_ENV` defensively.
|
|
12
|
+
*
|
|
13
|
+
* Wrapped in try/catch so the bare `process` reference can't throw in runtimes
|
|
14
|
+
* without a `process` global (workerd without `nodejs_compat`). Bundlers that
|
|
15
|
+
* statically define `process.env.NODE_ENV` (Vite/esbuild in Workers builds —
|
|
16
|
+
* the same role the old `import.meta.env.DEV` signal served) replace this
|
|
17
|
+
* expression with a string constant at build time, so no runtime `process`
|
|
18
|
+
* global is needed in that path at all.
|
|
19
|
+
*/
|
|
20
|
+
function readNodeEnv(): string | undefined {
|
|
21
|
+
try {
|
|
22
|
+
return process.env.NODE_ENV;
|
|
23
|
+
} catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
10
28
|
/**
|
|
11
29
|
* Returns `true` when running in a development environment.
|
|
12
30
|
*
|
|
13
31
|
* Detection order:
|
|
14
|
-
* 1. `
|
|
15
|
-
*
|
|
32
|
+
* 1. `readNodeEnv() === "development"` — bundler-define-friendly read of
|
|
33
|
+
* `process.env.NODE_ENV` (see `readNodeEnv` above). This replaces the
|
|
34
|
+
* previous `import.meta.env.DEV` signal: `import.meta` is ESM-only syntax
|
|
35
|
+
* and is a hard syntax error when CJS consumers (ts-jest) compile this
|
|
36
|
+
* raw-TS package, and `env.ts` is reachable via the `@decocms/blocks/sdk`
|
|
37
|
+
* barrel, so any such consumer importing the barrel would fail to compile.
|
|
38
|
+
* 2. `NODE_ENV=development` read off `globalThis.process.env` — standard
|
|
39
|
+
* Node/Vite convention, for runtimes where `process` is a real global.
|
|
40
|
+
* 3. `DECO_PREVIEW=true` — explicit preview-mode override.
|
|
16
41
|
*
|
|
17
42
|
* The result is memoised after the first evaluation.
|
|
18
43
|
*/
|
|
@@ -21,11 +46,9 @@ export function isDevMode(): boolean {
|
|
|
21
46
|
|
|
22
47
|
const env = typeof globalThis.process !== "undefined" ? globalThis.process.env : undefined;
|
|
23
48
|
|
|
24
|
-
|
|
25
|
-
// In Miniflare/Workers, process.env is unavailable, so this is the reliable signal.
|
|
26
|
-
const vitaDev = !!(import.meta as unknown as { env?: { DEV?: boolean } }).env?.DEV;
|
|
49
|
+
const nodeEnvDev = readNodeEnv() === "development";
|
|
27
50
|
|
|
28
|
-
_isDev =
|
|
51
|
+
_isDev = nodeEnvDev || env?.NODE_ENV === "development" || env?.DECO_PREVIEW === "true";
|
|
29
52
|
|
|
30
53
|
return _isDev;
|
|
31
54
|
}
|
|
@@ -87,7 +87,7 @@ export interface RequestContextData {
|
|
|
87
87
|
// Storage
|
|
88
88
|
// -------------------------------------------------------------------------
|
|
89
89
|
|
|
90
|
-
import { isMobileUA } from "./
|
|
90
|
+
import { isMobileUA } from "./detectDevice";
|
|
91
91
|
|
|
92
92
|
const BOT_RE =
|
|
93
93
|
/bot|crawl|spider|slurp|bingpreview|facebookexternalhit|linkedinbot|twitterbot|whatsapp|telegram|googlebot|yandex|baidu|duckduck/i;
|
package/src/sdk/useDevice.ts
CHANGED
|
@@ -21,52 +21,32 @@
|
|
|
21
21
|
* ```
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import { detectDevice } from "./detectDevice";
|
|
24
25
|
import { RequestContext } from "./requestContext";
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
// The pure UA-parsing half (`Device`, `MOBILE_RE`, `TABLET_RE`,
|
|
28
|
+
// `isMobileUA`, `detectDevice`) lives in `./detectDevice` — a leaf with no
|
|
29
|
+
// React and no RequestContext — and is re-exported below for backward
|
|
30
|
+
// compatibility. Modules that can land in a Next.js route-handler graph
|
|
31
|
+
// (`cms/sectionMixins.ts`, `sdk/requestContext.ts`) MUST import that leaf
|
|
32
|
+
// directly, not this file: this file also re-exports the `"use client"`
|
|
33
|
+
// context half, which crashes route handlers (see ./detectDevice's doc
|
|
34
|
+
// comment for the vendored-RSC-React mechanics).
|
|
35
|
+
export { type Device, detectDevice, isMobileUA, MOBILE_RE, TABLET_RE } from "./detectDevice";
|
|
27
36
|
|
|
28
37
|
// `DeviceContext`, the `useDevice()` hook, and `DeviceProvider` live in
|
|
29
38
|
// `./useDeviceContext` (a `"use client"` file) and are re-exported below for
|
|
30
39
|
// full backward compatibility with this module's public API — see that
|
|
31
|
-
// file's doc comment for why the split exists.
|
|
32
|
-
// *this* file is a plain, hook-free function, safe to import from
|
|
33
|
-
// server-only code (e.g. `cms/sectionMixins.ts`'s `withDevice()`/
|
|
34
|
-
// `withMobile()` loader mixins) without dragging a client-only hook/context
|
|
35
|
-
// boundary into a Server Component's module graph.
|
|
40
|
+
// file's doc comment for why the split exists.
|
|
36
41
|
export { DeviceContext, useDevice, DeviceProvider } from "./useDeviceContext";
|
|
37
42
|
|
|
38
|
-
// Android phones include "Mobile" in their UA; Android tablets do not.
|
|
39
|
-
// Check TABLET_RE first so `android(?!.*mobile)` captures tablets before
|
|
40
|
-
// the MOBILE_RE `android.*mobile` branch matches phones.
|
|
41
|
-
export const MOBILE_RE = /mobile|android.*mobile|iphone|ipod|webos|blackberry|opera mini|iemobile/i;
|
|
42
|
-
export const TABLET_RE = /ipad|tablet|kindle|silk|playbook|android(?!.*mobile)/i;
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Simple mobile-or-not check (mobile + tablet = true).
|
|
46
|
-
* Use this for cache key splitting or any context where you
|
|
47
|
-
* only need a mobile/desktop binary decision.
|
|
48
|
-
*/
|
|
49
|
-
export function isMobileUA(userAgent: string): boolean {
|
|
50
|
-
return MOBILE_RE.test(userAgent) || TABLET_RE.test(userAgent);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Detect device type from a User-Agent string.
|
|
55
|
-
* Pure function — no side effects, works anywhere.
|
|
56
|
-
*/
|
|
57
|
-
export function detectDevice(userAgent: string): Device {
|
|
58
|
-
if (TABLET_RE.test(userAgent)) return "tablet";
|
|
59
|
-
if (MOBILE_RE.test(userAgent)) return "mobile";
|
|
60
|
-
return "desktop";
|
|
61
|
-
}
|
|
62
|
-
|
|
63
43
|
/**
|
|
64
44
|
* Resolve the current device from the ambient runtime (RequestContext on the
|
|
65
45
|
* server, `navigator.userAgent` on the client) — no React hooks involved.
|
|
66
46
|
* Used both by the plain `check*()` helpers below and by `useDevice()`/
|
|
67
47
|
* `DeviceProvider` in `./useDeviceContext`.
|
|
68
48
|
*/
|
|
69
|
-
export function resolveDeviceFromRuntime():
|
|
49
|
+
export function resolveDeviceFromRuntime(): ReturnType<typeof detectDevice> {
|
|
70
50
|
// Server: use RequestContext UA header
|
|
71
51
|
if (typeof document === "undefined") {
|
|
72
52
|
const ctx = RequestContext.current;
|