@decocms/apps 1.1.2 → 1.3.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.
Files changed (50) hide show
  1. package/package.json +14 -2
  2. package/vtex/client.ts +1 -1
  3. package/vtex/commerceLoaders.ts +1 -1
  4. package/vtex/inline-loaders/productDetailsPage.ts +1 -0
  5. package/vtex/manifest.gen.ts +2 -0
  6. package/vtex/utils/transform.ts +71 -1
  7. package/website/client.ts +20 -0
  8. package/website/components/Analytics.tsx +146 -0
  9. package/website/components/Seo.tsx +139 -0
  10. package/website/components/Theme.tsx +47 -0
  11. package/website/components/Video.tsx +44 -0
  12. package/website/flags/audience.ts +56 -0
  13. package/website/flags/everyone.ts +19 -0
  14. package/website/flags/flag.ts +15 -0
  15. package/website/flags/multivariate/image.ts +11 -0
  16. package/website/flags/multivariate/message.ts +11 -0
  17. package/website/flags/multivariate/page.ts +16 -0
  18. package/website/flags/multivariate/section.ts +16 -0
  19. package/website/flags/multivariate.ts +1 -0
  20. package/website/index.ts +22 -0
  21. package/website/loaders/environment.ts +45 -0
  22. package/website/loaders/fonts/googleFonts.ts +119 -0
  23. package/website/loaders/fonts/local.ts +85 -0
  24. package/website/loaders/secret.ts +60 -0
  25. package/website/loaders/secretString.ts +18 -0
  26. package/website/manifest.gen.ts +31 -0
  27. package/website/matchers/always.ts +12 -0
  28. package/website/matchers/cookie.ts +33 -0
  29. package/website/matchers/cron.ts +109 -0
  30. package/website/matchers/date.ts +29 -0
  31. package/website/matchers/device.ts +40 -0
  32. package/website/matchers/environment.ts +21 -0
  33. package/website/matchers/host.ts +25 -0
  34. package/website/matchers/location.ts +113 -0
  35. package/website/matchers/multi.ts +24 -0
  36. package/website/matchers/negate.ts +21 -0
  37. package/website/matchers/never.ts +12 -0
  38. package/website/matchers/pathname.ts +69 -0
  39. package/website/matchers/queryString.ts +98 -0
  40. package/website/matchers/random.ts +24 -0
  41. package/website/matchers/site.ts +21 -0
  42. package/website/matchers/userAgent.ts +23 -0
  43. package/website/mod.ts +48 -0
  44. package/website/sections/Analytics/Analytics.tsx +7 -0
  45. package/website/sections/Seo/Seo.tsx +14 -0
  46. package/website/sections/Seo/SeoV2.tsx +45 -0
  47. package/website/types.ts +125 -0
  48. package/website/utils/html.ts +1 -0
  49. package/website/utils/location.ts +20 -0
  50. package/website/utils/multivariate.ts +20 -0
@@ -0,0 +1,16 @@
1
+ import type { MultivariateFlag } from "../../types";
2
+ import multivariate, { type MultivariateProps } from "../../utils/multivariate";
3
+
4
+ /**
5
+ * Section type placeholder — the actual Section type is defined by the framework.
6
+ */
7
+ type Section = unknown;
8
+
9
+ /**
10
+ * @title Page Variants
11
+ */
12
+ export default function PageVariants(
13
+ props: MultivariateProps<Section[]>,
14
+ ): MultivariateFlag<Section[]> {
15
+ return multivariate(props);
16
+ }
@@ -0,0 +1,16 @@
1
+ import type { MultivariateFlag } from "../../types";
2
+ import multivariate, { type MultivariateProps } from "../../utils/multivariate";
3
+
4
+ /**
5
+ * Section type placeholder — the actual Section type is defined by the framework.
6
+ */
7
+ type Section = unknown;
8
+
9
+ /**
10
+ * @title Section Variants
11
+ */
12
+ export default function SectionVariants(
13
+ props: MultivariateProps<Section>,
14
+ ): MultivariateFlag<Section> {
15
+ return multivariate(props);
16
+ }
@@ -0,0 +1 @@
1
+ export { default } from "./multivariate/page";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Public API for the website app.
3
+ */
4
+
5
+ export { configureWebsite, getWebsiteConfig } from "./client";
6
+ // App
7
+ export { configure } from "./mod";
8
+
9
+ // Types
10
+ export type {
11
+ FlagObj,
12
+ Font,
13
+ ImageWidget,
14
+ MatchContext,
15
+ Matcher,
16
+ MultivariateFlag,
17
+ OGType,
18
+ SeoConfig,
19
+ Variable,
20
+ Variant,
21
+ WebsiteConfig,
22
+ } from "./types";
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @title Environment
3
+ * @hideOption true
4
+ */
5
+ export interface Environment {
6
+ /** @ignore */
7
+ get: () => string | null;
8
+ }
9
+
10
+ export interface Props {
11
+ /**
12
+ * @title Environment Value
13
+ */
14
+ value: string;
15
+ /**
16
+ * @title Environment Name
17
+ * @description Used in dev mode as a environment variable (should not contain spaces or special characters)
18
+ * @pattern ^[a-zA-Z_][a-zA-Z0-9_]*$
19
+ */
20
+ name?: string;
21
+ }
22
+
23
+ const getEnvironment = (props: Props): string | null => {
24
+ const name = props?.name;
25
+ if (name && process.env[name] !== undefined) {
26
+ return process.env[name]!;
27
+ }
28
+ const value = props?.value;
29
+ if (!value) {
30
+ return null;
31
+ }
32
+ return value;
33
+ };
34
+
35
+ /**
36
+ * @title Environment
37
+ */
38
+ export default function EnvironmentLoader(props: Props): Environment {
39
+ const environmentValue = getEnvironment(props);
40
+ return {
41
+ get: (): string | null => {
42
+ return environmentValue;
43
+ },
44
+ };
45
+ }
@@ -0,0 +1,119 @@
1
+ import type { Font } from "../../types";
2
+
3
+ interface Props {
4
+ fonts: GoogleFont[];
5
+ }
6
+
7
+ /**
8
+ * @title {{weight}} {{#italic}}Italic{{/italic}}{{^italic}}{{/italic}}
9
+ */
10
+ interface FontVariation {
11
+ weight: "100" | "200" | "300" | "400" | "500" | "600" | "700" | "800" | "900";
12
+ italic?: boolean;
13
+ }
14
+
15
+ /** @titleBy family */
16
+ interface GoogleFont {
17
+ family: string;
18
+ variations: FontVariation[];
19
+ }
20
+
21
+ const getFontVariations = (variations: FontVariation[]) => {
22
+ if (variations.length === 0) {
23
+ return "";
24
+ }
25
+
26
+ let hasItalic = false;
27
+ const sortedVariations = [...variations]
28
+ .sort((a, b) => {
29
+ a.italic ??= false;
30
+ b.italic ??= false;
31
+
32
+ if (a.italic !== b.italic) {
33
+ hasItalic = true;
34
+ if (a.italic) return 1;
35
+ if (!a.italic) return -1;
36
+ }
37
+
38
+ return Number.parseInt(a.weight, 10) - Number.parseInt(b.weight, 10);
39
+ })
40
+ .filter(
41
+ (item, index, self) =>
42
+ index === self.findIndex((t) => t.weight === item.weight && t.italic === item.italic),
43
+ );
44
+
45
+ const variants: string[] = [];
46
+
47
+ for (const { weight, italic } of sortedVariations) {
48
+ if (!hasItalic) {
49
+ variants.push(weight);
50
+ continue;
51
+ }
52
+ variants.push(`${italic ? "1," : "0,"}${weight}`);
53
+ }
54
+
55
+ return `:${hasItalic ? "ital," : ""}wght@${variants.join(";")}`;
56
+ };
57
+
58
+ const NEW_BROWSER_KEY = {
59
+ "User-Agent":
60
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36",
61
+ };
62
+
63
+ const OLD_BROWSER_KEY = {
64
+ "User-Agent": "deco-cx/1.0",
65
+ };
66
+
67
+ const loader = async (props: Props): Promise<Font> => {
68
+ const { fonts = [] } = props;
69
+
70
+ if (fonts.length === 0) {
71
+ return { family: "", styleSheet: "" };
72
+ }
73
+
74
+ const url = new URL("https://fonts.googleapis.com/css2?display=swap");
75
+
76
+ const reduced = fonts.reduce(
77
+ (acc, font) => {
78
+ const { family, variations } = font;
79
+ acc[family] = acc[family] ?? { family, variations: [] };
80
+ acc[family].variations = [...acc[family].variations, ...variations];
81
+ return acc;
82
+ },
83
+ {} as Record<string, GoogleFont>,
84
+ );
85
+
86
+ for (const font of Object.values(reduced)) {
87
+ url.searchParams.append("family", `${font.family}${getFontVariations(font.variations)}`);
88
+ }
89
+
90
+ const logFontError = (label: string, fontUrl: URL, e: unknown) => {
91
+ const message = e instanceof Error ? e.message : String(e);
92
+ const short = message.length > 300 ? `${message.slice(0, 300)}…` : message;
93
+ console.error(`Error fetching font (${label}): ${fontUrl.toString()} - ${short}`);
94
+ };
95
+
96
+ const sheets = await Promise.all([
97
+ fetch(url, { headers: OLD_BROWSER_KEY })
98
+ .then((res) => res.text())
99
+ .catch((e) => {
100
+ logFontError("OLD_UA", url, e);
101
+ return "";
102
+ }),
103
+ fetch(url, { headers: NEW_BROWSER_KEY })
104
+ .then((res) => res.text())
105
+ .catch((e) => {
106
+ logFontError("NEW_UA", url, e);
107
+ return "";
108
+ }),
109
+ ]);
110
+
111
+ const styleSheet = sheets.join("\n");
112
+
113
+ return {
114
+ family: Object.keys(reduced).join(", "),
115
+ styleSheet,
116
+ };
117
+ };
118
+
119
+ export default loader;
@@ -0,0 +1,85 @@
1
+ import type { Font } from "../../types";
2
+
3
+ interface Props {
4
+ fonts: LocalFont[];
5
+ }
6
+
7
+ /**
8
+ * @title {{weight}} {{#italic}}Italic{{/italic}}{{^italic}}{{/italic}}
9
+ */
10
+ interface FontVariation {
11
+ weight: "100" | "200" | "300" | "400" | "500" | "600" | "700" | "800" | "900";
12
+ italic?: boolean;
13
+ /** @format file */
14
+ src: string;
15
+ }
16
+
17
+ const getFontFormat = (src: string): string => {
18
+ const clean = src.toLowerCase().split(/[?#]/, 1)[0];
19
+ const extension = clean.slice(clean.lastIndexOf(".") + 1);
20
+ switch (extension) {
21
+ case "woff2":
22
+ return "woff2";
23
+ case "woff":
24
+ return "woff";
25
+ case "ttf":
26
+ return "truetype";
27
+ case "otf":
28
+ return "opentype";
29
+ case "eot":
30
+ return "embedded-opentype";
31
+ case "svg":
32
+ return "svg";
33
+ default:
34
+ return "truetype";
35
+ }
36
+ };
37
+
38
+ /** @titleBy family */
39
+ interface LocalFont {
40
+ family: string;
41
+ variations: FontVariation[];
42
+ }
43
+
44
+ const loader = (props: Props): Font => {
45
+ const { fonts = [] } = props;
46
+
47
+ const reduced = fonts.reduce(
48
+ (acc, font) => {
49
+ const { family, variations } = font;
50
+ acc[family] = acc[family] ?? { family, variations: [] };
51
+ acc[family].variations = [...acc[family].variations, ...variations];
52
+ return acc;
53
+ },
54
+ {} as Record<string, LocalFont>,
55
+ );
56
+
57
+ const fontFaces: string[] = [];
58
+
59
+ for (const font of Object.values(reduced)) {
60
+ for (const variation of font.variations) {
61
+ const { weight, italic = false, src } = variation;
62
+ const fontStyle = italic ? "italic" : "normal";
63
+ const format = getFontFormat(src);
64
+
65
+ const fontFace = `@font-face {
66
+ font-family: '${font.family}';
67
+ font-style: ${fontStyle};
68
+ font-weight: ${weight};
69
+ font-display: swap;
70
+ src: url(${encodeURI(src)}) format('${format}');
71
+ }`;
72
+
73
+ fontFaces.push(fontFace);
74
+ }
75
+ }
76
+
77
+ const styleSheet = fontFaces.join("\n");
78
+
79
+ return {
80
+ family: Object.keys(reduced).join(", "),
81
+ styleSheet,
82
+ };
83
+ };
84
+
85
+ export default loader;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @title Secret
3
+ * @hideOption true
4
+ */
5
+ export interface Secret {
6
+ /** @ignore */
7
+ get: () => string | null;
8
+ }
9
+
10
+ export interface Props {
11
+ /**
12
+ * @title Secret Value
13
+ * @format secret
14
+ */
15
+ encrypted: string;
16
+ /**
17
+ * @title Secret Name
18
+ * @description Used in dev mode as a environment variable (should not contain spaces or special characters)
19
+ * @pattern ^[a-zA-Z_][a-zA-Z0-9_]*$
20
+ */
21
+ name?: string;
22
+ }
23
+
24
+ /**
25
+ * Resolve a secret value.
26
+ * In local dev, reads from process.env using the `name` field.
27
+ * In production, the framework's ResolveSecretFn handles decryption
28
+ * before the value reaches this loader.
29
+ */
30
+ const getSecret = (props: Props): string | null => {
31
+ const name = props?.name;
32
+ if (name && process.env[name] !== undefined) {
33
+ return process.env[name]!;
34
+ }
35
+ const encrypted = props?.encrypted;
36
+ if (!encrypted) {
37
+ return null;
38
+ }
39
+ // In production, the encrypted value should already be resolved
40
+ // by the framework's ResolveSecretFn before reaching this loader.
41
+ // If we get here with an encrypted value in dev, warn.
42
+ if (process.env.NODE_ENV !== "production") {
43
+ console.warn(
44
+ `Secret "${name ?? "anonymous"}" has encrypted value but no env var set. Set ${name} in .env.`,
45
+ );
46
+ }
47
+ return encrypted;
48
+ };
49
+
50
+ /**
51
+ * @title Secret
52
+ */
53
+ export default function SecretLoader(props: Props): Secret {
54
+ const secretValue = getSecret(props);
55
+ return {
56
+ get: (): string | null => {
57
+ return secretValue;
58
+ },
59
+ };
60
+ }
@@ -0,0 +1,18 @@
1
+ import type { Secret } from "./secret";
2
+
3
+ export interface Props {
4
+ secret: Secret;
5
+ }
6
+
7
+ /**
8
+ * @title Secret String (use Secret instead)
9
+ */
10
+ export type SecretString = string | null;
11
+
12
+ /**
13
+ * @title Secret String
14
+ * @deprecated true
15
+ */
16
+ export default function ({ secret }: Props): SecretString {
17
+ return secret.get();
18
+ }
@@ -0,0 +1,31 @@
1
+ // AUTO-GENERATED by scripts/generate-manifests.ts — DO NOT EDIT
2
+ // This file is checked into source control and updated via: npm run generate:manifests
3
+ import * as loaders_environment from "./loaders/environment";
4
+ import * as loaders_fonts_googleFonts from "./loaders/fonts/googleFonts";
5
+ import * as loaders_fonts_local from "./loaders/fonts/local";
6
+ import * as loaders_secret from "./loaders/secret";
7
+ import * as loaders_secretString from "./loaders/secretString";
8
+
9
+ const sections_Analytics_Analytics = () => import("./sections/Analytics/Analytics");
10
+ const sections_Seo_Seo = () => import("./sections/Seo/Seo");
11
+ const sections_Seo_SeoV2 = () => import("./sections/Seo/SeoV2");
12
+
13
+ const manifest = {
14
+ name: "website",
15
+ loaders: {
16
+ "website/loaders/environment": loaders_environment,
17
+ "website/loaders/fonts/googleFonts": loaders_fonts_googleFonts,
18
+ "website/loaders/fonts/local": loaders_fonts_local,
19
+ "website/loaders/secret": loaders_secret,
20
+ "website/loaders/secretString": loaders_secretString,
21
+ },
22
+ actions: {},
23
+ sections: {
24
+ "website/sections/Analytics/Analytics": sections_Analytics_Analytics,
25
+ "website/sections/Seo/Seo": sections_Seo_Seo,
26
+ "website/sections/Seo/SeoV2": sections_Seo_SeoV2,
27
+ },
28
+ } as const;
29
+
30
+ export type Manifest = typeof manifest;
31
+ export default manifest;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @title Always
3
+ * @description Target all users
4
+ * @icon eye
5
+ */
6
+ const MatchAlways = () => {
7
+ return true;
8
+ };
9
+
10
+ export default MatchAlways;
11
+
12
+ export const cacheable = true;
@@ -0,0 +1,33 @@
1
+ import type { MatchContext } from "../types";
2
+
3
+ /**
4
+ * @title Cookie
5
+ */
6
+ export interface Props {
7
+ name: string;
8
+ value: string;
9
+ }
10
+
11
+ function parseCookies(headers: Headers): Record<string, string> {
12
+ const cookieHeader = headers.get("cookie") || "";
13
+ const cookies: Record<string, string> = {};
14
+ for (const pair of cookieHeader.split(";")) {
15
+ const [key, ...rest] = pair.split("=");
16
+ if (key) {
17
+ cookies[key.trim()] = rest.join("=").trim();
18
+ }
19
+ }
20
+ return cookies;
21
+ }
22
+
23
+ /**
24
+ * @title Cookie
25
+ * @description Target users that have a specific cookie
26
+ * @icon cookie
27
+ */
28
+ const MatchCookie = ({ name, value }: Props, { request }: MatchContext) => {
29
+ const cookies = parseCookies(request.headers);
30
+ return cookies[name] === value;
31
+ };
32
+
33
+ export default MatchCookie;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * @titleBy cron
3
+ */
4
+ export interface CronProps {
5
+ /**
6
+ * @format cron
7
+ * @example * 0-23 * * WED (At every minute past every hour from 0 through 23 on Wednesday.)
8
+ * @pattern ^(?:(?:(?:\*|(?:\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*)|[A-Z]+)(?:\/\d+)?)(?:\s+|$)){5}$
9
+ */
10
+ cron: string;
11
+ }
12
+
13
+ function nowWithMinutePrecision() {
14
+ const date = new Date();
15
+ date.setSeconds(0);
16
+ date.setMilliseconds(0);
17
+ date.setTime(Math.floor(date.getTime() / 1000) * 1000);
18
+ return date;
19
+ }
20
+
21
+ /**
22
+ * Minimal 5-field cron matcher (minute, hour, day-of-month, month, day-of-week).
23
+ * Supports: wildcards (*), ranges (1-5), lists (1,3,5), steps (star/2), and
24
+ * abbreviated weekday names (MON-SUN).
25
+ */
26
+ function matchesCron(cronExpr: string, date: Date): boolean {
27
+ const DAY_NAMES: Record<string, number> = {
28
+ SUN: 0,
29
+ MON: 1,
30
+ TUE: 2,
31
+ WED: 3,
32
+ THU: 4,
33
+ FRI: 5,
34
+ SAT: 6,
35
+ };
36
+
37
+ const fields = cronExpr.trim().split(/\s+/);
38
+ if (fields.length < 5) return false;
39
+
40
+ const values = [
41
+ date.getMinutes(),
42
+ date.getHours(),
43
+ date.getDate(),
44
+ date.getMonth() + 1,
45
+ date.getDay(),
46
+ ];
47
+
48
+ for (let i = 0; i < 5; i++) {
49
+ const field = fields[i];
50
+ const value = values[i];
51
+
52
+ if (!matchField(field, value, i === 4)) return false;
53
+ }
54
+ return true;
55
+
56
+ function matchField(field: string, value: number, isDow: boolean): boolean {
57
+ return field.split(",").some((part) => matchPart(part, value, isDow));
58
+ }
59
+
60
+ function matchPart(part: string, value: number, isDow: boolean): boolean {
61
+ const [rangeStr, stepStr] = part.split("/");
62
+ const step = stepStr ? Number.parseInt(stepStr, 10) : 1;
63
+
64
+ if (rangeStr === "*") {
65
+ return step === 1 || value % step === 0;
66
+ }
67
+
68
+ if (rangeStr.includes("-")) {
69
+ const [startStr, endStr] = rangeStr.split("-");
70
+ const start = parseVal(startStr, isDow);
71
+ const end = parseVal(endStr, isDow);
72
+ if (start <= end) {
73
+ if (value < start || value > end) return false;
74
+ } else {
75
+ // wrap-around (e.g. FRI-MON)
76
+ if (value < start && value > end) return false;
77
+ }
78
+ return step === 1 || (value - start + 100) % step === 0;
79
+ }
80
+
81
+ return parseVal(rangeStr, isDow) === value;
82
+ }
83
+
84
+ function parseVal(s: string, isDow: boolean): number {
85
+ if (isDow) {
86
+ const upper = s.toUpperCase();
87
+ if (upper in DAY_NAMES) return DAY_NAMES[upper];
88
+ // Cron allows 7 as alias for Sunday (0)
89
+ const n = Number.parseInt(s, 10);
90
+ return n === 7 ? 0 : n;
91
+ }
92
+ return Number.parseInt(s, 10);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * @title Cron
98
+ * @description Target users with precision using recurring schedules
99
+ * @icon refresh
100
+ */
101
+ const MatchCron = (props: CronProps) => {
102
+ if (!props?.cron) {
103
+ return false;
104
+ }
105
+ const minutePrecision = nowWithMinutePrecision();
106
+ return matchesCron(props.cron, minutePrecision);
107
+ };
108
+
109
+ export default MatchCron;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @titleBy start
3
+ */
4
+ export interface Props {
5
+ /**
6
+ * @format date-time
7
+ */
8
+ start?: string;
9
+ /**
10
+ * @format date-time
11
+ */
12
+ end?: string;
13
+ }
14
+
15
+ /**
16
+ * @title Date and Time
17
+ * @description Target users based on specific dates or date ranges, including specific times
18
+ * @icon calendar-event
19
+ */
20
+ const MatchDate = (props: Props) => {
21
+ const now = new Date();
22
+ const start = props.start ? now > new Date(props.start) : true;
23
+ const end = props.end ? now < new Date(props.end) : true;
24
+ return start && end;
25
+ };
26
+
27
+ export default MatchDate;
28
+
29
+ export const cacheable = true;
@@ -0,0 +1,40 @@
1
+ import type { MatchContext } from "../types";
2
+
3
+ /**
4
+ * @title {{{.}}}
5
+ */
6
+ export type Device = "mobile" | "tablet" | "desktop";
7
+
8
+ /**
9
+ * @title {{#mobile}}Mobile{{/mobile}} {{#tablet}}Tablet{{/tablet}} {{#desktop}}Desktop{{/desktop}}
10
+ */
11
+ export interface Props {
12
+ /** @title Mobile */
13
+ mobile?: boolean;
14
+ /** @title Tablet */
15
+ tablet?: boolean;
16
+ /** @title Desktop */
17
+ desktop?: boolean;
18
+ }
19
+
20
+ // backwards compatibility
21
+ interface OldProps {
22
+ devices: Device[];
23
+ }
24
+
25
+ /**
26
+ * @title Device
27
+ * @description Target users based on their device type, such as desktop, tablet, or mobile
28
+ * @icon device-mobile
29
+ */
30
+ const MatchDevice = ({ mobile, tablet, desktop, ...rest }: Props, { device }: MatchContext) => {
31
+ const devices = (rest as unknown as OldProps)?.devices ?? [];
32
+ if (mobile) devices.push("mobile");
33
+ if (tablet) devices.push("tablet");
34
+ if (desktop) devices.push("desktop");
35
+ return devices.includes(device);
36
+ };
37
+
38
+ export default MatchDevice;
39
+
40
+ export const cacheable = true;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @title {{{environment}}}
3
+ */
4
+ export interface Props {
5
+ environment: "production" | "development";
6
+ }
7
+
8
+ /**
9
+ * @title Environment
10
+ * @description Target users based from where they are accessing your site (development, testing, or production)
11
+ * @icon code
12
+ */
13
+ const MatchEnvironment = ({ environment }: Props) => {
14
+ const isProduction = process.env.NODE_ENV === "production";
15
+
16
+ return environment === "production" ? isProduction : !isProduction;
17
+ };
18
+
19
+ export default MatchEnvironment;
20
+
21
+ export const cacheable = true;
@@ -0,0 +1,25 @@
1
+ import type { MatchContext } from "../types";
2
+
3
+ /**
4
+ * @title {{{includes}}} {{{match}}}
5
+ */
6
+ export interface Props {
7
+ includes?: string;
8
+ match?: string;
9
+ }
10
+
11
+ /**
12
+ * @title Host
13
+ * @description Target users based on the domain or subdomain they are accessing your site from
14
+ * @icon world-www
15
+ */
16
+ const MatchHost = ({ includes, match }: Props, { request }: MatchContext) => {
17
+ const host = request.headers.get("host") || request.headers.get("origin") || "";
18
+ const regexMatch = match ? new RegExp(match).test(host) : true;
19
+ const includesFound = includes ? host.includes(includes) : true;
20
+ return regexMatch && includesFound;
21
+ };
22
+
23
+ export default MatchHost;
24
+
25
+ export const cacheable = true;