@brandlab-digital/site-next 3.0.0 → 3.1.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/dist/contact/form-client.d.ts +11 -0
- package/dist/contact/form-client.d.ts.map +1 -1
- package/dist/contact/form-client.js +25 -2
- package/dist/contact/form-client.js.map +1 -1
- package/dist/headers.d.ts +35 -1
- package/dist/headers.d.ts.map +1 -1
- package/dist/headers.js +62 -2
- package/dist/headers.js.map +1 -1
- package/dist/next-config.d.ts +13 -2
- package/dist/next-config.d.ts.map +1 -1
- package/dist/next-config.js +74 -1
- package/dist/next-config.js.map +1 -1
- package/package.json +2 -2
|
@@ -31,6 +31,8 @@ export interface UseBrandlabContactFormOptions {
|
|
|
31
31
|
* The form element's id and the lead diagnostics' form_id. Without it the form
|
|
32
32
|
* gets no id (the hook never takes a React id, so the form's own ids are
|
|
33
33
|
* unchanged), diagnostics use `contact-form` and error ids start `contact-form-`.
|
|
34
|
+
* The element keeps the id exactly as given; diagnostics report it as a slug
|
|
35
|
+
* (React `useId()` parts removed, see `contactFormSlug`).
|
|
34
36
|
*/
|
|
35
37
|
formId?: string;
|
|
36
38
|
/** Page to open after an accepted enquiry; `null` or omitted keeps the visitor on the page. */
|
|
@@ -116,6 +118,15 @@ export interface UseBrandlabContactFormResult {
|
|
|
116
118
|
/** Contract field keys accepted by contactFieldProps. */
|
|
117
119
|
fieldNames: ReadonlySet<string>;
|
|
118
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* The lead diagnostics' form_id for any formId. A valid slug is kept (trimmed and
|
|
123
|
+
* lowercased). Otherwise React `useId()` parts are removed, so an id made unique for
|
|
124
|
+
* the page still reports as one stable form, every run of other characters becomes
|
|
125
|
+
* one hyphen, and the result is at most 80 characters; `contact-form` when nothing
|
|
126
|
+
* is left. Without this, a formId such as `contact-form-_R_5e35f9db_` silently
|
|
127
|
+
* dropped lead_form_start and lead_form_error.
|
|
128
|
+
*/
|
|
129
|
+
export declare function contactFormSlug(formId: string): string;
|
|
119
130
|
/**
|
|
120
131
|
* Contact form behaviour for project-owned markup and copy. The form posts
|
|
121
132
|
* through fetch with FormData; before hydration the submit button stays
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form-client.d.ts","sourceRoot":"","sources":["../../src/contact/form-client.ts"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,uBAAuB,EAC5B,KAAK,mBAAmB,EACzB,MAAM,qCAAqC,CAAA;AAE5C,OAAO,EAOL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,OAAO,CAAA;AACd,OAAO,KAAK,EAAE,iBAAiB,EAAsB,MAAM,iBAAiB,CAAA;AAG5E,OAAO,EAEL,KAAK,oCAAoC,EACzC,KAAK,mCAAmC,EACzC,MAAM,yBAAyB,CAAA;AAChC,OAAO,EAEL,KAAK,sBAAsB,EAE5B,MAAM,aAAa,CAAA;AAEpB,iGAAiG;AACjG,MAAM,MAAM,6BAA6B,GAAG,OAAO,CAAC,mBAAmB,EAAE;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,CAAC,CAAA;AACtF,MAAM,MAAM,2BAA2B,GAAG,OAAO,CAAC,mBAAmB,EAAE;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,CAAC,CAAA;AAErF,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,eAAe,CAAA;IACrB;;;OAGG;IACH,QAAQ,EAAE,QAAQ,CAAA;CACnB;AAED,MAAM,WAAW,6BAA6B;IAC5C,uDAAuD;IACvD,QAAQ,EAAE,uBAAuB,CAAA;IACjC,wHAAwH;IACxH,SAAS,CAAC,EAAE,iBAAiB,CAAA;IAC7B,yDAAyD;IACzD,QAAQ,EAAE,MAAM,CAAA;IAChB,uDAAuD;IACvD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB
|
|
1
|
+
{"version":3,"file":"form-client.d.ts","sourceRoot":"","sources":["../../src/contact/form-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { DEFAULT_CONTACT_ATTACHMENT_POLICY, DOCUMENT_CONTACT_ATTACHMENT_POLICY, defineContactContract, } from '@brandlab-digital/site-core/contact';
|
|
3
3
|
import { normalizeBrandlabRoutePath } from '@brandlab-digital/site-core/routes';
|
|
4
4
|
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from 'react';
|
|
5
|
-
import { leadBindingFor } from '../lead-registry.js';
|
|
5
|
+
import { leadBindingFor, safeSlug } from '../lead-registry.js';
|
|
6
6
|
import { reportAcceptedLead } from '../lead-reporting.js';
|
|
7
7
|
import { useBrandlabContactAttachments, } from './attachments-client.js';
|
|
8
8
|
import { createContactHttpSubmissionController, } from './client.js';
|
|
@@ -22,6 +22,29 @@ const FOCUSABLE_INVALID_FIELD = [
|
|
|
22
22
|
const subscribeToNothing = () => () => undefined;
|
|
23
23
|
const getClientSnapshot = () => true;
|
|
24
24
|
const getServerSnapshot = () => false;
|
|
25
|
+
/** React `useId()` values: `_R_1a_`/`_r_1a_` (19.2), `«r1a»` (19.0 and 19.1), `:r1a:` (18). */
|
|
26
|
+
const REACT_ID = /_[rR]_[0-9A-Za-z]*_|«[rR][0-9A-Za-z]*»|:[rR][0-9A-Za-z]*:/g;
|
|
27
|
+
/**
|
|
28
|
+
* The lead diagnostics' form_id for any formId. A valid slug is kept (trimmed and
|
|
29
|
+
* lowercased). Otherwise React `useId()` parts are removed, so an id made unique for
|
|
30
|
+
* the page still reports as one stable form, every run of other characters becomes
|
|
31
|
+
* one hyphen, and the result is at most 80 characters; `contact-form` when nothing
|
|
32
|
+
* is left. Without this, a formId such as `contact-form-_R_5e35f9db_` silently
|
|
33
|
+
* dropped lead_form_start and lead_form_error.
|
|
34
|
+
*/
|
|
35
|
+
export function contactFormSlug(formId) {
|
|
36
|
+
const slug = safeSlug(String(formId));
|
|
37
|
+
if (slug)
|
|
38
|
+
return slug;
|
|
39
|
+
const derived = String(formId)
|
|
40
|
+
.replace(REACT_ID, '-')
|
|
41
|
+
.toLowerCase()
|
|
42
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
43
|
+
.replace(/^-+/, '')
|
|
44
|
+
.slice(0, 80)
|
|
45
|
+
.replace(/-+$/, '');
|
|
46
|
+
return derived || 'contact-form';
|
|
47
|
+
}
|
|
25
48
|
function safePagePath(pagePath) {
|
|
26
49
|
try {
|
|
27
50
|
return normalizeBrandlabRoutePath(pagePath.split(/[?#]/, 1)[0] || '/');
|
|
@@ -61,7 +84,7 @@ export function useBrandlabContactForm(options) {
|
|
|
61
84
|
assertLifecycleOptions(options);
|
|
62
85
|
const hydrated = useSyncExternalStore(subscribeToNothing, getClientSnapshot, getServerSnapshot);
|
|
63
86
|
const formId = options.formId ?? 'contact-form';
|
|
64
|
-
const diagnosticsFormId = formId;
|
|
87
|
+
const diagnosticsFormId = contactFormSlug(formId);
|
|
65
88
|
const rawPagePath = options.pagePath;
|
|
66
89
|
const pagePath = safePagePath(rawPagePath);
|
|
67
90
|
const formRef = useRef(null);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form-client.js","sourceRoot":"","sources":["../../src/contact/form-client.ts"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\nimport {\n DEFAULT_CONTACT_ATTACHMENT_POLICY,\n DOCUMENT_CONTACT_ATTACHMENT_POLICY,\n defineContactContract,\n type BrandlabContactContract,\n type ContactActionResult,\n} from '@brandlab-digital/site-core/contact'\nimport { normalizeBrandlabRoutePath } from '@brandlab-digital/site-core/routes'\nimport {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n type FormEvent,\n type RefObject,\n} from 'react'\nimport type { BrandlabAnalytics, LeadFormErrorStage } from '../analytics.js'\nimport { leadBindingFor } from '../lead-registry.js'\nimport { reportAcceptedLead } from '../lead-reporting.js'\nimport {\n useBrandlabContactAttachments,\n type UseBrandlabContactAttachmentsOptions,\n type UseBrandlabContactAttachmentsResult,\n} from './attachments-client.js'\nimport {\n createContactHttpSubmissionController,\n type ContactSubmissionPhase,\n type ContactSubmissionState,\n} from './client.js'\n\n/** An accepted enquiry, or a silently accepted spam-protected submission (`accepted: false`). */\nexport type BrandlabContactAcceptedResult = Extract<ContactActionResult, { ok: true }>\nexport type BrandlabContactFailedResult = Extract<ContactActionResult, { ok: false }>\n\nexport interface BrandlabContactSubmitStart {\n form: HTMLFormElement\n /**\n * The fields about to be sent, read from the form. Add or replace entries\n * here (for example `service_interest`); prepared photos are appended after.\n */\n formData: FormData\n}\n\nexport interface UseBrandlabContactFormOptions {\n /** The contact contract from src/config/contact.ts. */\n contract: BrandlabContactContract\n /** The site's defineBrandlabAnalytics binding: the declared lead after each accepted enquiry, plus lead diagnostics. */\n analytics?: BrandlabAnalytics\n /** The current pathname, for example `usePathname()`. */\n pagePath: string\n /** Reviewed label for the enquiry email's page row. */\n pageLabel?: string\n /**\n * The form element's id and the lead diagnostics' form_id. Without it the form\n * gets no id (the hook never takes a React id, so the form's own ids are\n * unchanged), diagnostics use `contact-form` and error ids start `contact-form-`.\n */\n formId?: string\n /** Page to open after an accepted enquiry; `null` or omitted keeps the visitor on the page. */\n redirectTo?: string | null\n /** Client navigation such as `router.push`; defaults to `window.location.assign`. */\n navigate?: (href: string) => void\n /** Delay before the redirect, in milliseconds (default 1500); 0 navigates at once. */\n redirectDelayMs?: number\n /**\n * Runs when the visitor submits, before anything is sent: clear the form's\n * own notices, adjust `formData`, or return `false` to cancel (for example\n * after `form.reportValidity()`). An unchanged resubmission runs it too;\n * `retry()` does not.\n */\n onSubmitStart?: (submission: BrandlabContactSubmitStart) => void | false\n /**\n * Runs after every attempt, before onAccepted or onFailed: an accepted\n * enquiry, a silently accepted submission (even when ignored) or a failure.\n */\n onSettled?: (result: ContactActionResult) => void\n /** Runs after an accepted enquiry, once the form has been reset and before any redirect. */\n onAccepted?: (result: BrandlabContactAcceptedResult) => void\n /** Runs after a failed attempt; typed values and prepared photos are kept. */\n onFailed?: (result: BrandlabContactFailedResult) => void\n /**\n * A silently accepted submission (spam protection): `complete` (default)\n * resets, runs onAccepted and redirects like an accepted enquiry; `ignore`\n * leaves the form as it is and runs only onSettled.\n */\n silentAcceptance?: 'complete' | 'ignore'\n /** Photo preparation options; the policy follows the contract's attachment mode by default. */\n attachments?: UseBrandlabContactAttachmentsOptions\n /** Multipart field prefix for prepared files (default `attachments`, sent as `attachments[0]` …). */\n attachmentFieldPrefix?: string\n /** Live Consent Mode decision for ad_user_data; Enhanced Conversions stay off without it. */\n adUserDataConsent?: boolean\n contactEndpoint?: string\n fetcher?: typeof fetch\n}\n\n/**\n * Spread onto the `<form>`: `method=\"post\"` with no `action` or `encType`, so a\n * submission before hydration posts back to the page and never puts fields in\n * a URL; the hook submits through fetch. `id` only when `formId` is given.\n */\nexport interface BrandlabContactFormProps {\n id?: string\n ref: RefObject<HTMLFormElement | null>\n method: 'post'\n 'aria-busy': boolean\n onSubmit: (event: FormEvent<HTMLFormElement>) => void\n onFocusCapture: () => void\n}\n\n/** Spread onto the submit button: disabled until hydration, while sending or preparing photos. */\nexport interface BrandlabContactSubmitProps {\n type: 'submit'\n disabled: boolean\n}\n\nexport interface UseBrandlabContactFormResult {\n /** The `formId` option, or `contact-form` when none was given. */\n formId: string\n formProps: BrandlabContactFormProps\n submitProps: BrandlabContactSubmitProps\n /** False during server rendering and hydration. */\n hydrated: boolean\n /** The submit button's `disabled` (also in submitProps): until hydration, while sending or preparing photos. */\n submitDisabled: boolean\n pending: boolean\n phase: ContactSubmissionPhase\n result: ContactActionResult | undefined\n /** An accepted enquiry (a silently accepted spam-protected submission looks the same). */\n succeeded: boolean\n /** A retryable failure: retry(), or submitting the same fields again, reuses its submission ID. */\n canRetry: boolean\n fieldErrors: Readonly<Record<string, readonly string[]>>\n fieldError(name: string): string | undefined\n /** Id for a field's error message element: `${formId}-${name}-error`. */\n errorId(name: string): string\n /** Attach to the status element; it receives focus when an error has no invalid field. */\n statusRef: RefObject<HTMLElement | null>\n /** Resends the failed enquiry with the same submission ID. */\n retry(): Promise<ContactActionResult | undefined>\n attachments: UseBrandlabContactAttachmentsResult\n /** Contract field keys accepted by contactFieldProps. */\n fieldNames: ReadonlySet<string>\n}\n\nconst INITIAL_STATE: ContactSubmissionState = {\n phase: 'idle',\n pending: false,\n canRetry: false,\n}\nconst DEFAULT_REDIRECT_DELAY_MS = 1_500\nconst FOCUSABLE_INVALID_FIELD = [\n 'input[aria-invalid=\"true\"]:not([disabled]):not([type=\"hidden\"])',\n 'select[aria-invalid=\"true\"]:not([disabled])',\n 'textarea[aria-invalid=\"true\"]:not([disabled])',\n 'button[aria-invalid=\"true\"]:not([disabled])',\n '[aria-invalid=\"true\"][tabindex]:not([aria-disabled=\"true\"])',\n].join(',')\n\nconst subscribeToNothing = () => () => undefined\nconst getClientSnapshot = () => true\nconst getServerSnapshot = () => false\n\nfunction safePagePath(pagePath: string) {\n try {\n return normalizeBrandlabRoutePath(pagePath.split(/[?#]/, 1)[0] || '/')\n } catch {\n return '/'\n }\n}\n\nfunction errorStage(result: BrandlabContactFailedResult): LeadFormErrorStage {\n return result.code === 'validation' ? 'validation' : 'delivery'\n}\n\nfunction assertLifecycleOptions(options: UseBrandlabContactFormOptions) {\n const delay = options.redirectDelayMs\n if (delay !== undefined && (!Number.isInteger(delay) || delay < 0 || delay > 60_000)) {\n throw new TypeError('redirectDelayMs must be a whole number of milliseconds from 0 to 60000.')\n }\n if (options.silentAcceptance !== undefined && !['complete', 'ignore'].includes(options.silentAcceptance)) {\n throw new TypeError('silentAcceptance must be \"complete\" or \"ignore\".')\n }\n if (options.analytics !== undefined && !leadBindingFor(options.analytics)) {\n throw new TypeError('analytics must be the binding returned by defineBrandlabAnalytics.')\n }\n}\n\n/**\n * Contact form behaviour for project-owned markup and copy. The form posts\n * through fetch with FormData; before hydration the submit button stays\n * disabled. Typed values (uncontrolled fields) and prepared photos are kept\n * after any failure, retry() and an unchanged resubmission reuse the same\n * submission ID (so a resend cannot duplicate the email), the first invalid\n * field is focused, and an accepted enquiry resets the form and redirects.\n * The lead is reported exactly as the site's defineBrandlabAnalytics binding\n * declares, once per accepted enquiry. Browser validation is left exactly as\n * the form's own markup declares it.\n */\nexport function useBrandlabContactForm(options: UseBrandlabContactFormOptions): UseBrandlabContactFormResult {\n const contract = useMemo(() => defineContactContract(options.contract), [options.contract])\n assertLifecycleOptions(options)\n const hydrated = useSyncExternalStore(subscribeToNothing, getClientSnapshot, getServerSnapshot)\n const formId = options.formId ?? 'contact-form'\n const diagnosticsFormId = formId\n const rawPagePath = options.pagePath\n const pagePath = safePagePath(rawPagePath)\n const formRef = useRef<HTMLFormElement>(null)\n const statusRef = useRef<HTMLElement>(null)\n const [state, setState] = useState<ContactSubmissionState>(INITIAL_STATE)\n const lockRef = useRef(false)\n const startedRef = useRef(false)\n const redirectTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\n const latest = useRef(options)\n latest.current = options\n\n const policy = options.attachments?.policy ??\n (contract.attachments.mode === 'images-and-pdfs-optional'\n ? DOCUMENT_CONTACT_ATTACHMENT_POLICY\n : DEFAULT_CONTACT_ATTACHMENT_POLICY)\n const attachments = useBrandlabContactAttachments({ ...options.attachments, policy })\n const attachmentPrefix = options.attachmentFieldPrefix ?? 'attachments'\n\n const fieldNames = useMemo(() => new Set(contract.fields.map((field) => field.key)), [contract])\n const roles = useMemo(\n () => ({ fields: contract.fields.map((field) => ({ name: field.key, role: field.role })) }),\n [contract],\n )\n const { analytics, pageLabel, contactEndpoint, fetcher, adUserDataConsent } = options\n const controller = useMemo(() => {\n const lead = leadBindingFor(analytics)\n return createContactHttpSubmissionController({\n pagePath,\n ...(pageLabel ? { pageLabel } : {}),\n ...(contactEndpoint ? { contactEndpoint } : {}),\n ...(fetcher ? { fetcher } : {}),\n ...(lead\n ? {\n reportAccepted: (result, formData) => {\n reportAcceptedLead(lead, result, {\n pagePath: rawPagePath,\n formData,\n fieldRoles: roles,\n adUserDataConsent: adUserDataConsent === true,\n })\n },\n }\n : {}),\n onStateChange: setState,\n })\n }, [adUserDataConsent, analytics, contactEndpoint, fetcher, pageLabel, pagePath, rawPagePath, roles])\n\n useEffect(() => () => clearTimeout(redirectTimerRef.current), [])\n\n useEffect(() => {\n if (state.phase !== 'error') return\n const invalid = formRef.current?.querySelector<HTMLElement>(FOCUSABLE_INVALID_FIELD)\n ;(invalid ?? statusRef.current)?.focus()\n }, [state])\n\n const trackStart = useCallback(() => {\n if (startedRef.current) return\n startedRef.current = true\n latest.current.analytics?.trackLeadFormStart({ pagePath: rawPagePath, formId: diagnosticsFormId })\n }, [diagnosticsFormId, rawPagePath])\n\n const handleResult = useCallback(\n (result: ContactActionResult) => {\n const current = latest.current\n current.onSettled?.(result)\n if (!result.ok) {\n current.analytics?.trackLeadFormError({\n pagePath: rawPagePath,\n formId: diagnosticsFormId,\n stage: errorStage(result),\n errorCount: result.code === 'validation'\n ? Math.min(50, Math.max(1, Object.keys(result.fieldErrors ?? {}).length))\n : 1,\n })\n current.onFailed?.(result)\n return\n }\n if (!result.accepted && current.silentAcceptance === 'ignore') return\n formRef.current?.reset()\n attachments.clearImages()\n current.onAccepted?.(result)\n const redirectTo = current.redirectTo\n if (!redirectTo) return\n const navigate = current.navigate ?? ((href: string) => window.location.assign(href))\n const delay = current.redirectDelayMs ?? DEFAULT_REDIRECT_DELAY_MS\n clearTimeout(redirectTimerRef.current)\n if (delay === 0) {\n navigate(redirectTo)\n return\n }\n redirectTimerRef.current = setTimeout(() => navigate(redirectTo), delay)\n },\n [attachments, diagnosticsFormId, rawPagePath],\n )\n\n const send = useCallback(\n async (attempt: () => Promise<ContactActionResult>) => {\n lockRef.current = true\n try {\n const result = await attempt()\n handleResult(result)\n return result\n } finally {\n lockRef.current = false\n }\n },\n [handleResult],\n )\n\n const onSubmit = useCallback(\n (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault()\n if (lockRef.current || attachments.isProcessing) return\n const form = event.currentTarget\n const formData = new FormData(form)\n if (latest.current.onSubmitStart?.({ form, formData }) === false) return\n trackStart()\n attachments.clearErrorMessage()\n attachments.images.forEach((image, index) => {\n formData.append(`${attachmentPrefix}[${index}]`, image.file)\n })\n // The controller reports every failure as a result; nothing is left to handle here.\n send(() => controller.submit(formData)).catch(() => undefined)\n },\n [attachmentPrefix, attachments, controller, send, trackStart],\n )\n\n const retry = useCallback(async () => {\n if (lockRef.current || !controller.getState().canRetry) return undefined\n return send(() => controller.retry())\n }, [controller, send])\n\n const fieldErrors = state.result && !state.result.ok && state.result.code === 'validation'\n ? state.result.fieldErrors ?? {}\n : {}\n\n const submitDisabled = !hydrated || state.pending || attachments.isProcessing\n return {\n formId,\n formProps: {\n ...(options.formId ? { id: options.formId } : {}),\n ref: formRef,\n method: 'post',\n 'aria-busy': state.pending || attachments.isProcessing,\n onSubmit,\n onFocusCapture: trackStart,\n },\n submitProps: { type: 'submit', disabled: submitDisabled },\n hydrated,\n submitDisabled,\n pending: state.pending,\n phase: state.phase,\n result: state.result,\n succeeded: state.result?.ok === true,\n canRetry: state.canRetry,\n fieldErrors,\n fieldError: (name: string) => fieldErrors[name]?.[0],\n errorId: (name: string) => `${formId}-${name}-error`,\n statusRef,\n retry,\n attachments,\n fieldNames,\n }\n}\n\nexport interface ContactFieldPropsOptions {\n /** Ids of help text already describing the field; the error id is appended. */\n describedBy?: string\n /** Defaults to disabled while an enquiry is sending. */\n disabled?: boolean\n}\n\n/**\n * Props for one uncontrolled contract field: name, disabled while sending and\n * the error relationship. It never adds value, required or length attributes,\n * so the form's existing validation is unchanged.\n */\nexport function contactFieldProps(\n form: UseBrandlabContactFormResult,\n name: string,\n options: ContactFieldPropsOptions = {},\n) {\n if (!form.fieldNames.has(name)) {\n throw new Error(`\"${name}\" is not a field in the contact contract.`)\n }\n const error = form.fieldError(name)\n const describedBy = [options.describedBy, error ? form.errorId(name) : undefined]\n .filter(Boolean)\n .join(' ')\n return {\n name,\n disabled: options.disabled ?? form.pending,\n 'aria-invalid': error ? (true as const) : undefined,\n 'aria-describedby': describedBy || undefined,\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"form-client.js","sourceRoot":"","sources":["../../src/contact/form-client.ts"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\nimport {\n DEFAULT_CONTACT_ATTACHMENT_POLICY,\n DOCUMENT_CONTACT_ATTACHMENT_POLICY,\n defineContactContract,\n type BrandlabContactContract,\n type ContactActionResult,\n} from '@brandlab-digital/site-core/contact'\nimport { normalizeBrandlabRoutePath } from '@brandlab-digital/site-core/routes'\nimport {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n type FormEvent,\n type RefObject,\n} from 'react'\nimport type { BrandlabAnalytics, LeadFormErrorStage } from '../analytics.js'\nimport { leadBindingFor, safeSlug } from '../lead-registry.js'\nimport { reportAcceptedLead } from '../lead-reporting.js'\nimport {\n useBrandlabContactAttachments,\n type UseBrandlabContactAttachmentsOptions,\n type UseBrandlabContactAttachmentsResult,\n} from './attachments-client.js'\nimport {\n createContactHttpSubmissionController,\n type ContactSubmissionPhase,\n type ContactSubmissionState,\n} from './client.js'\n\n/** An accepted enquiry, or a silently accepted spam-protected submission (`accepted: false`). */\nexport type BrandlabContactAcceptedResult = Extract<ContactActionResult, { ok: true }>\nexport type BrandlabContactFailedResult = Extract<ContactActionResult, { ok: false }>\n\nexport interface BrandlabContactSubmitStart {\n form: HTMLFormElement\n /**\n * The fields about to be sent, read from the form. Add or replace entries\n * here (for example `service_interest`); prepared photos are appended after.\n */\n formData: FormData\n}\n\nexport interface UseBrandlabContactFormOptions {\n /** The contact contract from src/config/contact.ts. */\n contract: BrandlabContactContract\n /** The site's defineBrandlabAnalytics binding: the declared lead after each accepted enquiry, plus lead diagnostics. */\n analytics?: BrandlabAnalytics\n /** The current pathname, for example `usePathname()`. */\n pagePath: string\n /** Reviewed label for the enquiry email's page row. */\n pageLabel?: string\n /**\n * The form element's id and the lead diagnostics' form_id. Without it the form\n * gets no id (the hook never takes a React id, so the form's own ids are\n * unchanged), diagnostics use `contact-form` and error ids start `contact-form-`.\n * The element keeps the id exactly as given; diagnostics report it as a slug\n * (React `useId()` parts removed, see `contactFormSlug`).\n */\n formId?: string\n /** Page to open after an accepted enquiry; `null` or omitted keeps the visitor on the page. */\n redirectTo?: string | null\n /** Client navigation such as `router.push`; defaults to `window.location.assign`. */\n navigate?: (href: string) => void\n /** Delay before the redirect, in milliseconds (default 1500); 0 navigates at once. */\n redirectDelayMs?: number\n /**\n * Runs when the visitor submits, before anything is sent: clear the form's\n * own notices, adjust `formData`, or return `false` to cancel (for example\n * after `form.reportValidity()`). An unchanged resubmission runs it too;\n * `retry()` does not.\n */\n onSubmitStart?: (submission: BrandlabContactSubmitStart) => void | false\n /**\n * Runs after every attempt, before onAccepted or onFailed: an accepted\n * enquiry, a silently accepted submission (even when ignored) or a failure.\n */\n onSettled?: (result: ContactActionResult) => void\n /** Runs after an accepted enquiry, once the form has been reset and before any redirect. */\n onAccepted?: (result: BrandlabContactAcceptedResult) => void\n /** Runs after a failed attempt; typed values and prepared photos are kept. */\n onFailed?: (result: BrandlabContactFailedResult) => void\n /**\n * A silently accepted submission (spam protection): `complete` (default)\n * resets, runs onAccepted and redirects like an accepted enquiry; `ignore`\n * leaves the form as it is and runs only onSettled.\n */\n silentAcceptance?: 'complete' | 'ignore'\n /** Photo preparation options; the policy follows the contract's attachment mode by default. */\n attachments?: UseBrandlabContactAttachmentsOptions\n /** Multipart field prefix for prepared files (default `attachments`, sent as `attachments[0]` …). */\n attachmentFieldPrefix?: string\n /** Live Consent Mode decision for ad_user_data; Enhanced Conversions stay off without it. */\n adUserDataConsent?: boolean\n contactEndpoint?: string\n fetcher?: typeof fetch\n}\n\n/**\n * Spread onto the `<form>`: `method=\"post\"` with no `action` or `encType`, so a\n * submission before hydration posts back to the page and never puts fields in\n * a URL; the hook submits through fetch. `id` only when `formId` is given.\n */\nexport interface BrandlabContactFormProps {\n id?: string\n ref: RefObject<HTMLFormElement | null>\n method: 'post'\n 'aria-busy': boolean\n onSubmit: (event: FormEvent<HTMLFormElement>) => void\n onFocusCapture: () => void\n}\n\n/** Spread onto the submit button: disabled until hydration, while sending or preparing photos. */\nexport interface BrandlabContactSubmitProps {\n type: 'submit'\n disabled: boolean\n}\n\nexport interface UseBrandlabContactFormResult {\n /** The `formId` option, or `contact-form` when none was given. */\n formId: string\n formProps: BrandlabContactFormProps\n submitProps: BrandlabContactSubmitProps\n /** False during server rendering and hydration. */\n hydrated: boolean\n /** The submit button's `disabled` (also in submitProps): until hydration, while sending or preparing photos. */\n submitDisabled: boolean\n pending: boolean\n phase: ContactSubmissionPhase\n result: ContactActionResult | undefined\n /** An accepted enquiry (a silently accepted spam-protected submission looks the same). */\n succeeded: boolean\n /** A retryable failure: retry(), or submitting the same fields again, reuses its submission ID. */\n canRetry: boolean\n fieldErrors: Readonly<Record<string, readonly string[]>>\n fieldError(name: string): string | undefined\n /** Id for a field's error message element: `${formId}-${name}-error`. */\n errorId(name: string): string\n /** Attach to the status element; it receives focus when an error has no invalid field. */\n statusRef: RefObject<HTMLElement | null>\n /** Resends the failed enquiry with the same submission ID. */\n retry(): Promise<ContactActionResult | undefined>\n attachments: UseBrandlabContactAttachmentsResult\n /** Contract field keys accepted by contactFieldProps. */\n fieldNames: ReadonlySet<string>\n}\n\nconst INITIAL_STATE: ContactSubmissionState = {\n phase: 'idle',\n pending: false,\n canRetry: false,\n}\nconst DEFAULT_REDIRECT_DELAY_MS = 1_500\nconst FOCUSABLE_INVALID_FIELD = [\n 'input[aria-invalid=\"true\"]:not([disabled]):not([type=\"hidden\"])',\n 'select[aria-invalid=\"true\"]:not([disabled])',\n 'textarea[aria-invalid=\"true\"]:not([disabled])',\n 'button[aria-invalid=\"true\"]:not([disabled])',\n '[aria-invalid=\"true\"][tabindex]:not([aria-disabled=\"true\"])',\n].join(',')\n\nconst subscribeToNothing = () => () => undefined\nconst getClientSnapshot = () => true\nconst getServerSnapshot = () => false\n\n/** React `useId()` values: `_R_1a_`/`_r_1a_` (19.2), `«r1a»` (19.0 and 19.1), `:r1a:` (18). */\nconst REACT_ID = /_[rR]_[0-9A-Za-z]*_|«[rR][0-9A-Za-z]*»|:[rR][0-9A-Za-z]*:/g\n\n/**\n * The lead diagnostics' form_id for any formId. A valid slug is kept (trimmed and\n * lowercased). Otherwise React `useId()` parts are removed, so an id made unique for\n * the page still reports as one stable form, every run of other characters becomes\n * one hyphen, and the result is at most 80 characters; `contact-form` when nothing\n * is left. Without this, a formId such as `contact-form-_R_5e35f9db_` silently\n * dropped lead_form_start and lead_form_error.\n */\nexport function contactFormSlug(formId: string): string {\n const slug = safeSlug(String(formId))\n if (slug) return slug\n const derived = String(formId)\n .replace(REACT_ID, '-')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+/, '')\n .slice(0, 80)\n .replace(/-+$/, '')\n return derived || 'contact-form'\n}\n\nfunction safePagePath(pagePath: string) {\n try {\n return normalizeBrandlabRoutePath(pagePath.split(/[?#]/, 1)[0] || '/')\n } catch {\n return '/'\n }\n}\n\nfunction errorStage(result: BrandlabContactFailedResult): LeadFormErrorStage {\n return result.code === 'validation' ? 'validation' : 'delivery'\n}\n\nfunction assertLifecycleOptions(options: UseBrandlabContactFormOptions) {\n const delay = options.redirectDelayMs\n if (delay !== undefined && (!Number.isInteger(delay) || delay < 0 || delay > 60_000)) {\n throw new TypeError('redirectDelayMs must be a whole number of milliseconds from 0 to 60000.')\n }\n if (options.silentAcceptance !== undefined && !['complete', 'ignore'].includes(options.silentAcceptance)) {\n throw new TypeError('silentAcceptance must be \"complete\" or \"ignore\".')\n }\n if (options.analytics !== undefined && !leadBindingFor(options.analytics)) {\n throw new TypeError('analytics must be the binding returned by defineBrandlabAnalytics.')\n }\n}\n\n/**\n * Contact form behaviour for project-owned markup and copy. The form posts\n * through fetch with FormData; before hydration the submit button stays\n * disabled. Typed values (uncontrolled fields) and prepared photos are kept\n * after any failure, retry() and an unchanged resubmission reuse the same\n * submission ID (so a resend cannot duplicate the email), the first invalid\n * field is focused, and an accepted enquiry resets the form and redirects.\n * The lead is reported exactly as the site's defineBrandlabAnalytics binding\n * declares, once per accepted enquiry. Browser validation is left exactly as\n * the form's own markup declares it.\n */\nexport function useBrandlabContactForm(options: UseBrandlabContactFormOptions): UseBrandlabContactFormResult {\n const contract = useMemo(() => defineContactContract(options.contract), [options.contract])\n assertLifecycleOptions(options)\n const hydrated = useSyncExternalStore(subscribeToNothing, getClientSnapshot, getServerSnapshot)\n const formId = options.formId ?? 'contact-form'\n const diagnosticsFormId = contactFormSlug(formId)\n const rawPagePath = options.pagePath\n const pagePath = safePagePath(rawPagePath)\n const formRef = useRef<HTMLFormElement>(null)\n const statusRef = useRef<HTMLElement>(null)\n const [state, setState] = useState<ContactSubmissionState>(INITIAL_STATE)\n const lockRef = useRef(false)\n const startedRef = useRef(false)\n const redirectTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\n const latest = useRef(options)\n latest.current = options\n\n const policy = options.attachments?.policy ??\n (contract.attachments.mode === 'images-and-pdfs-optional'\n ? DOCUMENT_CONTACT_ATTACHMENT_POLICY\n : DEFAULT_CONTACT_ATTACHMENT_POLICY)\n const attachments = useBrandlabContactAttachments({ ...options.attachments, policy })\n const attachmentPrefix = options.attachmentFieldPrefix ?? 'attachments'\n\n const fieldNames = useMemo(() => new Set(contract.fields.map((field) => field.key)), [contract])\n const roles = useMemo(\n () => ({ fields: contract.fields.map((field) => ({ name: field.key, role: field.role })) }),\n [contract],\n )\n const { analytics, pageLabel, contactEndpoint, fetcher, adUserDataConsent } = options\n const controller = useMemo(() => {\n const lead = leadBindingFor(analytics)\n return createContactHttpSubmissionController({\n pagePath,\n ...(pageLabel ? { pageLabel } : {}),\n ...(contactEndpoint ? { contactEndpoint } : {}),\n ...(fetcher ? { fetcher } : {}),\n ...(lead\n ? {\n reportAccepted: (result, formData) => {\n reportAcceptedLead(lead, result, {\n pagePath: rawPagePath,\n formData,\n fieldRoles: roles,\n adUserDataConsent: adUserDataConsent === true,\n })\n },\n }\n : {}),\n onStateChange: setState,\n })\n }, [adUserDataConsent, analytics, contactEndpoint, fetcher, pageLabel, pagePath, rawPagePath, roles])\n\n useEffect(() => () => clearTimeout(redirectTimerRef.current), [])\n\n useEffect(() => {\n if (state.phase !== 'error') return\n const invalid = formRef.current?.querySelector<HTMLElement>(FOCUSABLE_INVALID_FIELD)\n ;(invalid ?? statusRef.current)?.focus()\n }, [state])\n\n const trackStart = useCallback(() => {\n if (startedRef.current) return\n startedRef.current = true\n latest.current.analytics?.trackLeadFormStart({ pagePath: rawPagePath, formId: diagnosticsFormId })\n }, [diagnosticsFormId, rawPagePath])\n\n const handleResult = useCallback(\n (result: ContactActionResult) => {\n const current = latest.current\n current.onSettled?.(result)\n if (!result.ok) {\n current.analytics?.trackLeadFormError({\n pagePath: rawPagePath,\n formId: diagnosticsFormId,\n stage: errorStage(result),\n errorCount: result.code === 'validation'\n ? Math.min(50, Math.max(1, Object.keys(result.fieldErrors ?? {}).length))\n : 1,\n })\n current.onFailed?.(result)\n return\n }\n if (!result.accepted && current.silentAcceptance === 'ignore') return\n formRef.current?.reset()\n attachments.clearImages()\n current.onAccepted?.(result)\n const redirectTo = current.redirectTo\n if (!redirectTo) return\n const navigate = current.navigate ?? ((href: string) => window.location.assign(href))\n const delay = current.redirectDelayMs ?? DEFAULT_REDIRECT_DELAY_MS\n clearTimeout(redirectTimerRef.current)\n if (delay === 0) {\n navigate(redirectTo)\n return\n }\n redirectTimerRef.current = setTimeout(() => navigate(redirectTo), delay)\n },\n [attachments, diagnosticsFormId, rawPagePath],\n )\n\n const send = useCallback(\n async (attempt: () => Promise<ContactActionResult>) => {\n lockRef.current = true\n try {\n const result = await attempt()\n handleResult(result)\n return result\n } finally {\n lockRef.current = false\n }\n },\n [handleResult],\n )\n\n const onSubmit = useCallback(\n (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault()\n if (lockRef.current || attachments.isProcessing) return\n const form = event.currentTarget\n const formData = new FormData(form)\n if (latest.current.onSubmitStart?.({ form, formData }) === false) return\n trackStart()\n attachments.clearErrorMessage()\n attachments.images.forEach((image, index) => {\n formData.append(`${attachmentPrefix}[${index}]`, image.file)\n })\n // The controller reports every failure as a result; nothing is left to handle here.\n send(() => controller.submit(formData)).catch(() => undefined)\n },\n [attachmentPrefix, attachments, controller, send, trackStart],\n )\n\n const retry = useCallback(async () => {\n if (lockRef.current || !controller.getState().canRetry) return undefined\n return send(() => controller.retry())\n }, [controller, send])\n\n const fieldErrors = state.result && !state.result.ok && state.result.code === 'validation'\n ? state.result.fieldErrors ?? {}\n : {}\n\n const submitDisabled = !hydrated || state.pending || attachments.isProcessing\n return {\n formId,\n formProps: {\n ...(options.formId ? { id: options.formId } : {}),\n ref: formRef,\n method: 'post',\n 'aria-busy': state.pending || attachments.isProcessing,\n onSubmit,\n onFocusCapture: trackStart,\n },\n submitProps: { type: 'submit', disabled: submitDisabled },\n hydrated,\n submitDisabled,\n pending: state.pending,\n phase: state.phase,\n result: state.result,\n succeeded: state.result?.ok === true,\n canRetry: state.canRetry,\n fieldErrors,\n fieldError: (name: string) => fieldErrors[name]?.[0],\n errorId: (name: string) => `${formId}-${name}-error`,\n statusRef,\n retry,\n attachments,\n fieldNames,\n }\n}\n\nexport interface ContactFieldPropsOptions {\n /** Ids of help text already describing the field; the error id is appended. */\n describedBy?: string\n /** Defaults to disabled while an enquiry is sending. */\n disabled?: boolean\n}\n\n/**\n * Props for one uncontrolled contract field: name, disabled while sending and\n * the error relationship. It never adds value, required or length attributes,\n * so the form's existing validation is unchanged.\n */\nexport function contactFieldProps(\n form: UseBrandlabContactFormResult,\n name: string,\n options: ContactFieldPropsOptions = {},\n) {\n if (!form.fieldNames.has(name)) {\n throw new Error(`\"${name}\" is not a field in the contact contract.`)\n }\n const error = form.fieldError(name)\n const describedBy = [options.describedBy, error ? form.errorId(name) : undefined]\n .filter(Boolean)\n .join(' ')\n return {\n name,\n disabled: options.disabled ?? form.pending,\n 'aria-invalid': error ? (true as const) : undefined,\n 'aria-describedby': describedBy || undefined,\n }\n}\n"]}
|
package/dist/headers.d.ts
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
*
|
|
12
12
|
* The production X-Robots-Tag carries snippet-display controls only; per-page
|
|
13
13
|
* indexation stays owned by Metadata.robots.
|
|
14
|
+
*
|
|
15
|
+
* Declared public folders of static files (`staticAssetCache`) are served with
|
|
16
|
+
* `public, max-age=31536000, immutable` by production builds; Previews and
|
|
17
|
+
* `next dev` never cache them.
|
|
14
18
|
*/
|
|
15
19
|
import type { BrandlabDeployment } from '@brandlab-digital/site-core/site';
|
|
16
20
|
export interface BrandlabHeader {
|
|
@@ -26,6 +30,8 @@ export declare const BRANDLAB_DEFAULT_X_ROBOTS_TAG = "max-snippet:-1, max-image-
|
|
|
26
30
|
export declare const BRANDLAB_PREVIEW_X_ROBOTS_TAG = "noindex, nofollow, noarchive";
|
|
27
31
|
export declare const BRANDLAB_PREVIEW_CACHE_CONTROL = "private, no-store, max-age=0";
|
|
28
32
|
export declare const BRANDLAB_HSTS_VALUE = "max-age=31536000";
|
|
33
|
+
/** Cache-Control for the declared static public folders on production builds. */
|
|
34
|
+
export declare const BRANDLAB_STATIC_ASSET_CACHE_CONTROL = "public, max-age=31536000, immutable";
|
|
29
35
|
export interface BrandlabHstsExpansion {
|
|
30
36
|
includeSubDomains: true;
|
|
31
37
|
preload?: true;
|
|
@@ -53,5 +59,33 @@ export declare function buildBrandlabContentSecurityPolicy(options: {
|
|
|
53
59
|
contentSecurityPolicyExtensions?: BrandlabContentSecurityPolicyExtensions;
|
|
54
60
|
}): string;
|
|
55
61
|
export declare function buildBrandlabSecurityHeaders(options: BrandlabSecurityHeaderOptions): BrandlabHeader[];
|
|
56
|
-
export
|
|
62
|
+
export interface BrandlabStaticAssetFolder {
|
|
63
|
+
/** The declared source, for example `/images/:path*`. */
|
|
64
|
+
source: string;
|
|
65
|
+
/** The public folder's path segments, for example `['images']`. */
|
|
66
|
+
folder: readonly string[];
|
|
67
|
+
/**
|
|
68
|
+
* The emitted rule's source: file names with an extension below the folder,
|
|
69
|
+
* such as `/images/:path(.+\.[A-Za-z0-9]+)`. The folder itself and
|
|
70
|
+
* extensionless paths, where a page would answer, never match.
|
|
71
|
+
*/
|
|
72
|
+
fileSource: string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Validates `staticAssetCache`: at most 10 public folders, each written as Next's
|
|
76
|
+
* source `/<folder>/:path*` (one to four literal segments, then one catch-all
|
|
77
|
+
* parameter, at most 100 characters). `/`, `/api`, `/_next` and `/_vercel` are
|
|
78
|
+
* rejected.
|
|
79
|
+
*/
|
|
80
|
+
export declare function parseBrandlabStaticAssetCache(sources: unknown): BrandlabStaticAssetFolder[];
|
|
81
|
+
export interface BrandlabHeaderRuleOptions extends BrandlabSecurityHeaderOptions {
|
|
82
|
+
/**
|
|
83
|
+
* Public folders of static files that never change in place, for example
|
|
84
|
+
* `['/images/:path*', '/fonts/:path*']`: production builds serve their files with
|
|
85
|
+
* `public, max-age=31536000, immutable`. A file replaced under the same name stays
|
|
86
|
+
* cached in browsers for up to a year, so give a changed file a new name.
|
|
87
|
+
*/
|
|
88
|
+
staticAssetCache?: readonly string[];
|
|
89
|
+
}
|
|
90
|
+
export declare function buildBrandlabHeaderRules(options: BrandlabHeaderRuleOptions): BrandlabHeaderRule[];
|
|
57
91
|
//# sourceMappingURL=headers.d.ts.map
|
package/dist/headers.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../src/headers.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../src/headers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAEnB,MAAM,kCAAkC,CAAA;AAEzC,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,cAAc,EAAE,CAAA;CAC1B;AAED,eAAO,MAAM,mCAAmC,6CACJ,CAAA;AAE5C,eAAO,MAAM,6BAA6B,kEACuB,CAAA;AAEjE,eAAO,MAAM,6BAA6B,iCAAiC,CAAA;AAE3E,eAAO,MAAM,8BAA8B,iCAAiC,CAAA;AAE5E,eAAO,MAAM,mBAAmB,qBACZ,CAAA;AAEpB,iFAAiF;AACjF,eAAO,MAAM,mCAAmC,wCAAwC,CAAA;AAExF,MAAM,WAAW,qBAAqB;IACpC,iBAAiB,EAAE,IAAI,CAAA;IACvB,OAAO,CAAC,EAAE,IAAI,CAAA;IACd,mEAAmE;IACnE,iBAAiB,EAAE,MAAM,CAAA;CAC1B;AAoBD,MAAM,WAAW,uCAAuC;IACtD,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC7B,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC7B;AAED,MAAM,WAAW,6BAA6B;IAC5C,mDAAmD;IACnD,UAAU,EAAE,kBAAkB,CAAA;IAC9B,IAAI,CAAC,EAAE,qBAAqB,CAAA;IAC5B,wEAAwE;IACxE,+BAA+B,CAAC,EAAE,uCAAuC,CAAA;IACzE,2FAA2F;IAC3F,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,wEAAwE;IACxE,YAAY,CAAC,EAAE,SAAS,cAAc,EAAE,CAAA;CACzC;AAcD,wBAAgB,kCAAkC,CAAC,OAAO,EAAE;IAC1D,UAAU,EAAE,kBAAkB,CAAA;IAC9B,+BAA+B,CAAC,EAAE,uCAAuC,CAAA;CAC1E,UAsDA;AAiDD,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,6BAA6B,GACrC,cAAc,EAAE,CAmDlB;AAOD,MAAM,WAAW,yBAAyB;IACxC,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAA;IACd,mEAAmE;IACnE,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;IACzB;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,OAAO,GAAG,yBAAyB,EAAE,CA4B3F;AAED,MAAM,WAAW,yBAA0B,SAAQ,6BAA6B;IAC9E;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACrC;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB,GAAG,kBAAkB,EAAE,CAuBjG"}
|
package/dist/headers.js
CHANGED
|
@@ -11,12 +11,18 @@
|
|
|
11
11
|
*
|
|
12
12
|
* The production X-Robots-Tag carries snippet-display controls only; per-page
|
|
13
13
|
* indexation stays owned by Metadata.robots.
|
|
14
|
+
*
|
|
15
|
+
* Declared public folders of static files (`staticAssetCache`) are served with
|
|
16
|
+
* `public, max-age=31536000, immutable` by production builds; Previews and
|
|
17
|
+
* `next dev` never cache them.
|
|
14
18
|
*/
|
|
15
19
|
export const BRANDLAB_DEFAULT_PERMISSIONS_POLICY = 'camera=(), microphone=(), geolocation=()';
|
|
16
20
|
export const BRANDLAB_DEFAULT_X_ROBOTS_TAG = 'max-snippet:-1, max-image-preview:large, max-video-preview:-1';
|
|
17
21
|
export const BRANDLAB_PREVIEW_X_ROBOTS_TAG = 'noindex, nofollow, noarchive';
|
|
18
22
|
export const BRANDLAB_PREVIEW_CACHE_CONTROL = 'private, no-store, max-age=0';
|
|
19
23
|
export const BRANDLAB_HSTS_VALUE = 'max-age=31536000';
|
|
24
|
+
/** Cache-Control for the declared static public folders on production builds. */
|
|
25
|
+
export const BRANDLAB_STATIC_ASSET_CACHE_CONTROL = 'public, max-age=31536000, immutable';
|
|
20
26
|
const HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
21
27
|
const PROTECTED_HEADER_NAMES = new Set([
|
|
22
28
|
'cache-control',
|
|
@@ -183,12 +189,66 @@ export function buildBrandlabSecurityHeaders(options) {
|
|
|
183
189
|
...(preview ? [{ key: 'Cache-Control', value: BRANDLAB_PREVIEW_CACHE_CONTROL }] : []),
|
|
184
190
|
];
|
|
185
191
|
}
|
|
192
|
+
const STATIC_ASSET_SOURCE = /^\/((?:[A-Za-z0-9][A-Za-z0-9._-]*\/){1,4}):([A-Za-z_][A-Za-z0-9_]*)\*$/;
|
|
193
|
+
const STATIC_ASSET_RESERVED = new Set(['api', '_next', '_vercel']);
|
|
194
|
+
const STATIC_ASSET_MAX_SOURCES = 10;
|
|
195
|
+
const STATIC_ASSET_MAX_LENGTH = 100;
|
|
196
|
+
/**
|
|
197
|
+
* Validates `staticAssetCache`: at most 10 public folders, each written as Next's
|
|
198
|
+
* source `/<folder>/:path*` (one to four literal segments, then one catch-all
|
|
199
|
+
* parameter, at most 100 characters). `/`, `/api`, `/_next` and `/_vercel` are
|
|
200
|
+
* rejected.
|
|
201
|
+
*/
|
|
202
|
+
export function parseBrandlabStaticAssetCache(sources) {
|
|
203
|
+
if (!Array.isArray(sources)) {
|
|
204
|
+
throw new Error('staticAssetCache must list public folders such as /images/:path*.');
|
|
205
|
+
}
|
|
206
|
+
if (sources.length > STATIC_ASSET_MAX_SOURCES) {
|
|
207
|
+
throw new Error(`staticAssetCache lists at most ${STATIC_ASSET_MAX_SOURCES} public folders.`);
|
|
208
|
+
}
|
|
209
|
+
const seen = new Set();
|
|
210
|
+
return sources.map((source, index) => {
|
|
211
|
+
const field = `staticAssetCache[${index}]`;
|
|
212
|
+
if (typeof source !== 'string' || source.length > STATIC_ASSET_MAX_LENGTH) {
|
|
213
|
+
throw new Error(`${field} must be a source of at most ${STATIC_ASSET_MAX_LENGTH} characters.`);
|
|
214
|
+
}
|
|
215
|
+
if (STATIC_ASSET_RESERVED.has(source.split('/')[1]?.toLowerCase() ?? '')) {
|
|
216
|
+
throw new Error(`${field} "${source}" is a Next or API path, not a public folder.`);
|
|
217
|
+
}
|
|
218
|
+
const match = STATIC_ASSET_SOURCE.exec(source);
|
|
219
|
+
if (!match) {
|
|
220
|
+
throw new Error(`${field} "${source}" must name a public folder as /<folder>/:path* (literal folder segments, then one catch-all parameter).`);
|
|
221
|
+
}
|
|
222
|
+
const folder = match[1].slice(0, -1).split('/');
|
|
223
|
+
const key = folder.join('/').toLowerCase();
|
|
224
|
+
if (seen.has(key))
|
|
225
|
+
throw new Error(`${field} repeats the /${folder.join('/')} folder.`);
|
|
226
|
+
seen.add(key);
|
|
227
|
+
return { source, folder, fileSource: `/${folder.join('/')}/:${match[2]}(.+\\.[A-Za-z0-9]+)` };
|
|
228
|
+
});
|
|
229
|
+
}
|
|
186
230
|
export function buildBrandlabHeaderRules(options) {
|
|
187
|
-
|
|
231
|
+
if (!options || typeof options !== 'object') {
|
|
232
|
+
throw new TypeError('Header options are required.');
|
|
233
|
+
}
|
|
234
|
+
const { staticAssetCache, ...security } = options;
|
|
235
|
+
const folders = staticAssetCache === undefined ? [] : parseBrandlabStaticAssetCache(staticAssetCache);
|
|
236
|
+
const rules = [
|
|
188
237
|
{
|
|
189
238
|
source: '/:path*',
|
|
190
|
-
headers: buildBrandlabSecurityHeaders(
|
|
239
|
+
headers: buildBrandlabSecurityHeaders(security),
|
|
191
240
|
},
|
|
192
241
|
];
|
|
242
|
+
// A production build outside a Preview; Previews keep private, no-store everywhere.
|
|
243
|
+
const { environment, productionBuild } = options.deployment;
|
|
244
|
+
if (productionBuild && environment !== 'preview') {
|
|
245
|
+
for (const folder of folders) {
|
|
246
|
+
rules.push({
|
|
247
|
+
source: folder.fileSource,
|
|
248
|
+
headers: [{ key: 'Cache-Control', value: BRANDLAB_STATIC_ASSET_CACHE_CONTROL }],
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return rules;
|
|
193
253
|
}
|
|
194
254
|
//# sourceMappingURL=headers.js.map
|
package/dist/headers.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"headers.js","sourceRoot":"","sources":["../src/headers.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Brandlab security/response headers: one canonical header set for every\n * client site, emitted by `defineBrandlabNextConfig`.\n *\n * The deployment decides everything environment-specific:\n * - a production build (`next build`, on Vercel or locally) enforces the\n * production Content Security Policy and sends HSTS;\n * - `next dev` sends a report-only development policy without HSTS;\n * - a Vercel Preview additionally sends `noindex, nofollow, noarchive` and\n * `private, no-store`, so previews never reach search results or caches.\n *\n * The production X-Robots-Tag carries snippet-display controls only; per-page\n * indexation stays owned by Metadata.robots.\n */\n\nimport type {\n BrandlabDeployment,\n BrandlabDeploymentEnvironment,\n} from '@brandlab-digital/site-core/site'\n\nexport interface BrandlabHeader {\n key: string\n value: string\n}\n\nexport interface BrandlabHeaderRule {\n source: string\n headers: BrandlabHeader[]\n}\n\nexport const BRANDLAB_DEFAULT_PERMISSIONS_POLICY =\n 'camera=(), microphone=(), geolocation=()'\n\nexport const BRANDLAB_DEFAULT_X_ROBOTS_TAG =\n 'max-snippet:-1, max-image-preview:large, max-video-preview:-1'\n\nexport const BRANDLAB_PREVIEW_X_ROBOTS_TAG = 'noindex, nofollow, noarchive'\n\nexport const BRANDLAB_PREVIEW_CACHE_CONTROL = 'private, no-store, max-age=0'\n\nexport const BRANDLAB_HSTS_VALUE =\n 'max-age=31536000'\n\nexport interface BrandlabHstsExpansion {\n includeSubDomains: true\n preload?: true\n /** Reviewed proof that every affected subdomain is HTTPS-ready. */\n readinessEvidence: string\n}\n\nconst HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/\nconst PROTECTED_HEADER_NAMES = new Set([\n 'cache-control',\n 'content-security-policy',\n 'content-security-policy-report-only',\n 'x-frame-options',\n 'x-content-type-options',\n 'referrer-policy',\n 'permissions-policy',\n 'x-robots-tag',\n 'strict-transport-security',\n])\nconst DEPLOYMENT_ENVIRONMENTS = new Set<BrandlabDeploymentEnvironment>([\n 'production',\n 'preview',\n 'development',\n])\n\nexport interface BrandlabContentSecurityPolicyExtensions {\n scriptSrc?: readonly string[]\n connectSrc?: readonly string[]\n frameSrc?: readonly string[]\n}\n\nexport interface BrandlabSecurityHeaderOptions {\n /** From resolveBrandlabDeployment(process.env). */\n deployment: BrandlabDeployment\n hsts?: BrandlabHstsExpansion\n /** Reviewed HTTPS origins needed by an approved project integration. */\n contentSecurityPolicyExtensions?: BrandlabContentSecurityPolicyExtensions\n /** Reviewed project exception, for example `geolocation=(self)` for a location feature. */\n permissionsPolicy?: string\n /** Reviewed project headers; they cannot replace a protected header. */\n extraHeaders?: readonly BrandlabHeader[]\n}\n\nfunction assertDeployment(deployment: unknown): asserts deployment is BrandlabDeployment {\n const candidate = deployment as Partial<BrandlabDeployment> | undefined\n if (\n !candidate ||\n typeof candidate !== 'object' ||\n !DEPLOYMENT_ENVIRONMENTS.has(candidate.environment as BrandlabDeploymentEnvironment) ||\n typeof candidate.productionBuild !== 'boolean'\n ) {\n throw new Error('Header rules require the deployment from resolveBrandlabDeployment(process.env).')\n }\n}\n\nexport function buildBrandlabContentSecurityPolicy(options: {\n deployment: BrandlabDeployment\n contentSecurityPolicyExtensions?: BrandlabContentSecurityPolicyExtensions\n}) {\n assertDeployment(options.deployment)\n const productionBuild = options.deployment.productionBuild\n\n const scriptSources = [\n \"'self'\",\n \"'unsafe-inline'\",\n 'https://www.googletagmanager.com',\n 'https://www.googleadservices.com',\n 'https://www.google.com',\n 'https://www.gstatic.com',\n 'https://pagead2.googlesyndication.com',\n 'https://googleads.g.doubleclick.net',\n 'https://challenges.cloudflare.com',\n ...normalizeCspExtensions(options.contentSecurityPolicyExtensions?.scriptSrc, 'scriptSrc'),\n ...(productionBuild ? [] : [\"'unsafe-eval'\", 'http://localhost:*']),\n ]\n const connectSources = [\n \"'self'\",\n 'https://www.google-analytics.com',\n 'https://*.google-analytics.com',\n 'https://www.googletagmanager.com',\n 'https://www.googleadservices.com',\n 'https://*.doubleclick.net',\n 'https://*.google.com',\n 'https://google.com',\n 'https://www.google.com.au',\n 'https://pagead2.googlesyndication.com',\n 'https://*.googletagmanager.com',\n 'https://*.analytics.google.com',\n 'https://challenges.cloudflare.com',\n ...normalizeCspExtensions(options.contentSecurityPolicyExtensions?.connectSrc, 'connectSrc'),\n ...(productionBuild ? [] : ['http://localhost:*', 'ws://localhost:*']),\n ]\n const directives = [\n \"default-src 'self'\",\n \"base-uri 'self'\",\n \"object-src 'none'\",\n \"frame-ancestors 'none'\",\n \"form-action 'self'\",\n `script-src ${scriptSources.join(' ')}`,\n \"style-src 'self' 'unsafe-inline'\",\n \"font-src 'self' data:\",\n \"img-src 'self' data: blob: https:\",\n `connect-src ${connectSources.join(' ')}`,\n `frame-src 'self' https://challenges.cloudflare.com https://www.googletagmanager.com https://www.google.com ${normalizeCspExtensions(\n options.contentSecurityPolicyExtensions?.frameSrc,\n 'frameSrc',\n ).join(' ')}`.trim(),\n \"manifest-src 'self'\",\n \"worker-src 'self' blob:\",\n ...(productionBuild ? ['upgrade-insecure-requests'] : []),\n ]\n return directives.join('; ')\n}\n\nfunction normalizeCspExtensions(\n sources: readonly string[] | undefined,\n field: keyof BrandlabContentSecurityPolicyExtensions,\n) {\n if (!sources) return []\n const normalized = sources.map((source) => {\n const value = source.trim().toLowerCase()\n if (!/^https:\\/\\/(?:\\*\\.)?[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?::\\d{1,5})?$/.test(value)) {\n throw new Error(`${field} entries must be HTTPS origins without paths or CSP keywords.`)\n }\n return value\n })\n if (new Set(normalized).size !== normalized.length) {\n throw new Error(`${field} entries must be unique.`)\n }\n return normalized\n}\n\nfunction assertHeaderValue(value: string, field: string) {\n if (!value || value.length > 4_096 || /[\\u0000-\\u001F\\u007F]/.test(value)) {\n throw new Error(`${field} must be a bounded single-line header value.`)\n }\n}\n\nfunction validateExtraHeaders(headers: readonly BrandlabHeader[]) {\n const names = new Set<string>()\n headers.forEach((header, index) => {\n if (!HEADER_NAME_PATTERN.test(header.key)) {\n throw new Error(`extraHeaders[${index}].key is invalid.`)\n }\n const name = header.key.toLowerCase()\n if (PROTECTED_HEADER_NAMES.has(name) || names.has(name)) {\n throw new Error(`extraHeaders[${index}] duplicates a protected or existing header.`)\n }\n names.add(name)\n assertHeaderValue(header.value, `extraHeaders[${index}].value`)\n })\n}\n\nconst HEADER_OPTION_KEYS = new Set([\n 'deployment',\n 'hsts',\n 'contentSecurityPolicyExtensions',\n 'permissionsPolicy',\n 'extraHeaders',\n])\n\nexport function buildBrandlabSecurityHeaders(\n options: BrandlabSecurityHeaderOptions,\n): BrandlabHeader[] {\n if (!options || typeof options !== 'object') {\n throw new TypeError('Header options are required.')\n }\n const unsupported = Object.keys(options).filter((key) => !HEADER_OPTION_KEYS.has(key))\n if (unsupported.length > 0) {\n throw new Error(\n `Header options contain unsupported keys: ${unsupported.join(', ')}. The deployment decides CSP, HSTS and X-Robots-Tag.`,\n )\n }\n assertDeployment(options.deployment)\n const { environment, productionBuild } = options.deployment\n const permissionsPolicy =\n options.permissionsPolicy ?? BRANDLAB_DEFAULT_PERMISSIONS_POLICY\n assertHeaderValue(permissionsPolicy, 'permissionsPolicy')\n validateExtraHeaders(options.extraHeaders ?? [])\n if (options.hsts) {\n assertHeaderValue(options.hsts.readinessEvidence, 'hsts.readinessEvidence')\n if (options.hsts.preload && !options.hsts.includeSubDomains) {\n throw new Error('HSTS preload requires includeSubDomains readiness.')\n }\n }\n const hstsValue = options.hsts\n ? `${BRANDLAB_HSTS_VALUE}; includeSubDomains${options.hsts.preload ? '; preload' : ''}`\n : BRANDLAB_HSTS_VALUE\n const preview = environment === 'preview'\n\n return [\n { key: 'X-Frame-Options', value: 'DENY' },\n { key: 'X-Content-Type-Options', value: 'nosniff' },\n { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },\n {\n key: 'Permissions-Policy',\n value: permissionsPolicy,\n },\n {\n key: 'X-Robots-Tag',\n value: preview ? BRANDLAB_PREVIEW_X_ROBOTS_TAG : BRANDLAB_DEFAULT_X_ROBOTS_TAG,\n },\n ...(productionBuild\n ? [{ key: 'Strict-Transport-Security', value: hstsValue }]\n : []),\n {\n key: productionBuild\n ? 'Content-Security-Policy'\n : 'Content-Security-Policy-Report-Only',\n value: buildBrandlabContentSecurityPolicy(options),\n },\n ...(options.extraHeaders ?? []).map((header) => ({ ...header })),\n ...(preview ? [{ key: 'Cache-Control', value: BRANDLAB_PREVIEW_CACHE_CONTROL }] : []),\n ]\n}\n\nexport function buildBrandlabHeaderRules(options: BrandlabSecurityHeaderOptions): BrandlabHeaderRule[] {\n return [\n {\n source: '/:path*',\n headers: buildBrandlabSecurityHeaders(options),\n },\n ]\n}\n"]}
|
|
1
|
+
{"version":3,"file":"headers.js","sourceRoot":"","sources":["../src/headers.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Brandlab security/response headers: one canonical header set for every\n * client site, emitted by `defineBrandlabNextConfig`.\n *\n * The deployment decides everything environment-specific:\n * - a production build (`next build`, on Vercel or locally) enforces the\n * production Content Security Policy and sends HSTS;\n * - `next dev` sends a report-only development policy without HSTS;\n * - a Vercel Preview additionally sends `noindex, nofollow, noarchive` and\n * `private, no-store`, so previews never reach search results or caches.\n *\n * The production X-Robots-Tag carries snippet-display controls only; per-page\n * indexation stays owned by Metadata.robots.\n *\n * Declared public folders of static files (`staticAssetCache`) are served with\n * `public, max-age=31536000, immutable` by production builds; Previews and\n * `next dev` never cache them.\n */\n\nimport type {\n BrandlabDeployment,\n BrandlabDeploymentEnvironment,\n} from '@brandlab-digital/site-core/site'\n\nexport interface BrandlabHeader {\n key: string\n value: string\n}\n\nexport interface BrandlabHeaderRule {\n source: string\n headers: BrandlabHeader[]\n}\n\nexport const BRANDLAB_DEFAULT_PERMISSIONS_POLICY =\n 'camera=(), microphone=(), geolocation=()'\n\nexport const BRANDLAB_DEFAULT_X_ROBOTS_TAG =\n 'max-snippet:-1, max-image-preview:large, max-video-preview:-1'\n\nexport const BRANDLAB_PREVIEW_X_ROBOTS_TAG = 'noindex, nofollow, noarchive'\n\nexport const BRANDLAB_PREVIEW_CACHE_CONTROL = 'private, no-store, max-age=0'\n\nexport const BRANDLAB_HSTS_VALUE =\n 'max-age=31536000'\n\n/** Cache-Control for the declared static public folders on production builds. */\nexport const BRANDLAB_STATIC_ASSET_CACHE_CONTROL = 'public, max-age=31536000, immutable'\n\nexport interface BrandlabHstsExpansion {\n includeSubDomains: true\n preload?: true\n /** Reviewed proof that every affected subdomain is HTTPS-ready. */\n readinessEvidence: string\n}\n\nconst HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/\nconst PROTECTED_HEADER_NAMES = new Set([\n 'cache-control',\n 'content-security-policy',\n 'content-security-policy-report-only',\n 'x-frame-options',\n 'x-content-type-options',\n 'referrer-policy',\n 'permissions-policy',\n 'x-robots-tag',\n 'strict-transport-security',\n])\nconst DEPLOYMENT_ENVIRONMENTS = new Set<BrandlabDeploymentEnvironment>([\n 'production',\n 'preview',\n 'development',\n])\n\nexport interface BrandlabContentSecurityPolicyExtensions {\n scriptSrc?: readonly string[]\n connectSrc?: readonly string[]\n frameSrc?: readonly string[]\n}\n\nexport interface BrandlabSecurityHeaderOptions {\n /** From resolveBrandlabDeployment(process.env). */\n deployment: BrandlabDeployment\n hsts?: BrandlabHstsExpansion\n /** Reviewed HTTPS origins needed by an approved project integration. */\n contentSecurityPolicyExtensions?: BrandlabContentSecurityPolicyExtensions\n /** Reviewed project exception, for example `geolocation=(self)` for a location feature. */\n permissionsPolicy?: string\n /** Reviewed project headers; they cannot replace a protected header. */\n extraHeaders?: readonly BrandlabHeader[]\n}\n\nfunction assertDeployment(deployment: unknown): asserts deployment is BrandlabDeployment {\n const candidate = deployment as Partial<BrandlabDeployment> | undefined\n if (\n !candidate ||\n typeof candidate !== 'object' ||\n !DEPLOYMENT_ENVIRONMENTS.has(candidate.environment as BrandlabDeploymentEnvironment) ||\n typeof candidate.productionBuild !== 'boolean'\n ) {\n throw new Error('Header rules require the deployment from resolveBrandlabDeployment(process.env).')\n }\n}\n\nexport function buildBrandlabContentSecurityPolicy(options: {\n deployment: BrandlabDeployment\n contentSecurityPolicyExtensions?: BrandlabContentSecurityPolicyExtensions\n}) {\n assertDeployment(options.deployment)\n const productionBuild = options.deployment.productionBuild\n\n const scriptSources = [\n \"'self'\",\n \"'unsafe-inline'\",\n 'https://www.googletagmanager.com',\n 'https://www.googleadservices.com',\n 'https://www.google.com',\n 'https://www.gstatic.com',\n 'https://pagead2.googlesyndication.com',\n 'https://googleads.g.doubleclick.net',\n 'https://challenges.cloudflare.com',\n ...normalizeCspExtensions(options.contentSecurityPolicyExtensions?.scriptSrc, 'scriptSrc'),\n ...(productionBuild ? [] : [\"'unsafe-eval'\", 'http://localhost:*']),\n ]\n const connectSources = [\n \"'self'\",\n 'https://www.google-analytics.com',\n 'https://*.google-analytics.com',\n 'https://www.googletagmanager.com',\n 'https://www.googleadservices.com',\n 'https://*.doubleclick.net',\n 'https://*.google.com',\n 'https://google.com',\n 'https://www.google.com.au',\n 'https://pagead2.googlesyndication.com',\n 'https://*.googletagmanager.com',\n 'https://*.analytics.google.com',\n 'https://challenges.cloudflare.com',\n ...normalizeCspExtensions(options.contentSecurityPolicyExtensions?.connectSrc, 'connectSrc'),\n ...(productionBuild ? [] : ['http://localhost:*', 'ws://localhost:*']),\n ]\n const directives = [\n \"default-src 'self'\",\n \"base-uri 'self'\",\n \"object-src 'none'\",\n \"frame-ancestors 'none'\",\n \"form-action 'self'\",\n `script-src ${scriptSources.join(' ')}`,\n \"style-src 'self' 'unsafe-inline'\",\n \"font-src 'self' data:\",\n \"img-src 'self' data: blob: https:\",\n `connect-src ${connectSources.join(' ')}`,\n `frame-src 'self' https://challenges.cloudflare.com https://www.googletagmanager.com https://www.google.com ${normalizeCspExtensions(\n options.contentSecurityPolicyExtensions?.frameSrc,\n 'frameSrc',\n ).join(' ')}`.trim(),\n \"manifest-src 'self'\",\n \"worker-src 'self' blob:\",\n ...(productionBuild ? ['upgrade-insecure-requests'] : []),\n ]\n return directives.join('; ')\n}\n\nfunction normalizeCspExtensions(\n sources: readonly string[] | undefined,\n field: keyof BrandlabContentSecurityPolicyExtensions,\n) {\n if (!sources) return []\n const normalized = sources.map((source) => {\n const value = source.trim().toLowerCase()\n if (!/^https:\\/\\/(?:\\*\\.)?[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?::\\d{1,5})?$/.test(value)) {\n throw new Error(`${field} entries must be HTTPS origins without paths or CSP keywords.`)\n }\n return value\n })\n if (new Set(normalized).size !== normalized.length) {\n throw new Error(`${field} entries must be unique.`)\n }\n return normalized\n}\n\nfunction assertHeaderValue(value: string, field: string) {\n if (!value || value.length > 4_096 || /[\\u0000-\\u001F\\u007F]/.test(value)) {\n throw new Error(`${field} must be a bounded single-line header value.`)\n }\n}\n\nfunction validateExtraHeaders(headers: readonly BrandlabHeader[]) {\n const names = new Set<string>()\n headers.forEach((header, index) => {\n if (!HEADER_NAME_PATTERN.test(header.key)) {\n throw new Error(`extraHeaders[${index}].key is invalid.`)\n }\n const name = header.key.toLowerCase()\n if (PROTECTED_HEADER_NAMES.has(name) || names.has(name)) {\n throw new Error(`extraHeaders[${index}] duplicates a protected or existing header.`)\n }\n names.add(name)\n assertHeaderValue(header.value, `extraHeaders[${index}].value`)\n })\n}\n\nconst HEADER_OPTION_KEYS = new Set([\n 'deployment',\n 'hsts',\n 'contentSecurityPolicyExtensions',\n 'permissionsPolicy',\n 'extraHeaders',\n])\n\nexport function buildBrandlabSecurityHeaders(\n options: BrandlabSecurityHeaderOptions,\n): BrandlabHeader[] {\n if (!options || typeof options !== 'object') {\n throw new TypeError('Header options are required.')\n }\n const unsupported = Object.keys(options).filter((key) => !HEADER_OPTION_KEYS.has(key))\n if (unsupported.length > 0) {\n throw new Error(\n `Header options contain unsupported keys: ${unsupported.join(', ')}. The deployment decides CSP, HSTS and X-Robots-Tag.`,\n )\n }\n assertDeployment(options.deployment)\n const { environment, productionBuild } = options.deployment\n const permissionsPolicy =\n options.permissionsPolicy ?? BRANDLAB_DEFAULT_PERMISSIONS_POLICY\n assertHeaderValue(permissionsPolicy, 'permissionsPolicy')\n validateExtraHeaders(options.extraHeaders ?? [])\n if (options.hsts) {\n assertHeaderValue(options.hsts.readinessEvidence, 'hsts.readinessEvidence')\n if (options.hsts.preload && !options.hsts.includeSubDomains) {\n throw new Error('HSTS preload requires includeSubDomains readiness.')\n }\n }\n const hstsValue = options.hsts\n ? `${BRANDLAB_HSTS_VALUE}; includeSubDomains${options.hsts.preload ? '; preload' : ''}`\n : BRANDLAB_HSTS_VALUE\n const preview = environment === 'preview'\n\n return [\n { key: 'X-Frame-Options', value: 'DENY' },\n { key: 'X-Content-Type-Options', value: 'nosniff' },\n { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },\n {\n key: 'Permissions-Policy',\n value: permissionsPolicy,\n },\n {\n key: 'X-Robots-Tag',\n value: preview ? BRANDLAB_PREVIEW_X_ROBOTS_TAG : BRANDLAB_DEFAULT_X_ROBOTS_TAG,\n },\n ...(productionBuild\n ? [{ key: 'Strict-Transport-Security', value: hstsValue }]\n : []),\n {\n key: productionBuild\n ? 'Content-Security-Policy'\n : 'Content-Security-Policy-Report-Only',\n value: buildBrandlabContentSecurityPolicy(options),\n },\n ...(options.extraHeaders ?? []).map((header) => ({ ...header })),\n ...(preview ? [{ key: 'Cache-Control', value: BRANDLAB_PREVIEW_CACHE_CONTROL }] : []),\n ]\n}\n\nconst STATIC_ASSET_SOURCE = /^\\/((?:[A-Za-z0-9][A-Za-z0-9._-]*\\/){1,4}):([A-Za-z_][A-Za-z0-9_]*)\\*$/\nconst STATIC_ASSET_RESERVED = new Set(['api', '_next', '_vercel'])\nconst STATIC_ASSET_MAX_SOURCES = 10\nconst STATIC_ASSET_MAX_LENGTH = 100\n\nexport interface BrandlabStaticAssetFolder {\n /** The declared source, for example `/images/:path*`. */\n source: string\n /** The public folder's path segments, for example `['images']`. */\n folder: readonly string[]\n /**\n * The emitted rule's source: file names with an extension below the folder,\n * such as `/images/:path(.+\\.[A-Za-z0-9]+)`. The folder itself and\n * extensionless paths, where a page would answer, never match.\n */\n fileSource: string\n}\n\n/**\n * Validates `staticAssetCache`: at most 10 public folders, each written as Next's\n * source `/<folder>/:path*` (one to four literal segments, then one catch-all\n * parameter, at most 100 characters). `/`, `/api`, `/_next` and `/_vercel` are\n * rejected.\n */\nexport function parseBrandlabStaticAssetCache(sources: unknown): BrandlabStaticAssetFolder[] {\n if (!Array.isArray(sources)) {\n throw new Error('staticAssetCache must list public folders such as /images/:path*.')\n }\n if (sources.length > STATIC_ASSET_MAX_SOURCES) {\n throw new Error(`staticAssetCache lists at most ${STATIC_ASSET_MAX_SOURCES} public folders.`)\n }\n const seen = new Set<string>()\n return sources.map((source: unknown, index) => {\n const field = `staticAssetCache[${index}]`\n if (typeof source !== 'string' || source.length > STATIC_ASSET_MAX_LENGTH) {\n throw new Error(`${field} must be a source of at most ${STATIC_ASSET_MAX_LENGTH} characters.`)\n }\n if (STATIC_ASSET_RESERVED.has(source.split('/')[1]?.toLowerCase() ?? '')) {\n throw new Error(`${field} \"${source}\" is a Next or API path, not a public folder.`)\n }\n const match = STATIC_ASSET_SOURCE.exec(source)\n if (!match) {\n throw new Error(\n `${field} \"${source}\" must name a public folder as /<folder>/:path* (literal folder segments, then one catch-all parameter).`,\n )\n }\n const folder = match[1]!.slice(0, -1).split('/')\n const key = folder.join('/').toLowerCase()\n if (seen.has(key)) throw new Error(`${field} repeats the /${folder.join('/')} folder.`)\n seen.add(key)\n return { source, folder, fileSource: `/${folder.join('/')}/:${match[2]}(.+\\\\.[A-Za-z0-9]+)` }\n })\n}\n\nexport interface BrandlabHeaderRuleOptions extends BrandlabSecurityHeaderOptions {\n /**\n * Public folders of static files that never change in place, for example\n * `['/images/:path*', '/fonts/:path*']`: production builds serve their files with\n * `public, max-age=31536000, immutable`. A file replaced under the same name stays\n * cached in browsers for up to a year, so give a changed file a new name.\n */\n staticAssetCache?: readonly string[]\n}\n\nexport function buildBrandlabHeaderRules(options: BrandlabHeaderRuleOptions): BrandlabHeaderRule[] {\n if (!options || typeof options !== 'object') {\n throw new TypeError('Header options are required.')\n }\n const { staticAssetCache, ...security } = options\n const folders = staticAssetCache === undefined ? [] : parseBrandlabStaticAssetCache(staticAssetCache)\n const rules: BrandlabHeaderRule[] = [\n {\n source: '/:path*',\n headers: buildBrandlabSecurityHeaders(security),\n },\n ]\n // A production build outside a Preview; Previews keep private, no-store everywhere.\n const { environment, productionBuild } = options.deployment\n if (productionBuild && environment !== 'preview') {\n for (const folder of folders) {\n rules.push({\n source: folder.fileSource,\n headers: [{ key: 'Cache-Control', value: BRANDLAB_STATIC_ASSET_CACHE_CONTROL }],\n })\n }\n }\n return rules\n}\n"]}
|
package/dist/next-config.d.ts
CHANGED
|
@@ -64,8 +64,18 @@ export interface BrandlabNextConfigOptions {
|
|
|
64
64
|
permissionsPolicy?: string;
|
|
65
65
|
/** Reviewed response headers that are already live; protected headers cannot be replaced. */
|
|
66
66
|
extraHeaders?: readonly BrandlabHeader[];
|
|
67
|
+
/**
|
|
68
|
+
* Public folders of static files that never change in place, written as Next
|
|
69
|
+
* sources such as `['/images/:path*', '/fonts/:path*']`. Production builds serve
|
|
70
|
+
* their files (names with an extension) with `public, max-age=31536000, immutable`;
|
|
71
|
+
* Previews keep `private, no-store`. Each folder must exist under `public/`, and
|
|
72
|
+
* no route under `src/app` or `app` may answer below it.
|
|
73
|
+
*/
|
|
74
|
+
staticAssetCache?: readonly string[];
|
|
67
75
|
/** Defaults to process.env; tests supply a fixed environment. */
|
|
68
76
|
environment?: BrandlabEnvironment;
|
|
77
|
+
/** The project root for the staticAssetCache checks; defaults to process.cwd(). */
|
|
78
|
+
projectRoot?: string;
|
|
69
79
|
}
|
|
70
80
|
/** Sharp's Linux binaries must travel with the contact route's image sanitiser. */
|
|
71
81
|
export declare const BRANDLAB_CONTACT_OUTPUT_FILE_TRACING: Readonly<{
|
|
@@ -89,8 +99,9 @@ export declare function buildBrandlabRedirects(options: {
|
|
|
89
99
|
*
|
|
90
100
|
* Sets poweredByHeader: false, WebP image output, Sharp output tracing for
|
|
91
101
|
* /api/contact, NEXT_PUBLIC_VERCEL_ENV for browser gates, the deployment's
|
|
92
|
-
* header rules
|
|
93
|
-
*
|
|
102
|
+
* header rules (with immutable caching for the declared static public folders on
|
|
103
|
+
* production builds) and the redirect table. Validation runs when the config
|
|
104
|
+
* loads, so an invalid table, header exception or static folder fails the build.
|
|
94
105
|
*/
|
|
95
106
|
export declare function defineBrandlabNextConfig(options: BrandlabNextConfigOptions & {
|
|
96
107
|
mdx?: undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"next-config.d.ts","sourceRoot":"","sources":["../src/next-config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"next-config.d.ts","sourceRoot":"","sources":["../src/next-config.ts"],"names":[],"mappings":"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"}
|
package/dist/next-config.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, posix } from 'node:path';
|
|
1
3
|
import { defineRedirects } from '@brandlab-digital/site-core/redirects';
|
|
2
4
|
import { resolveBrandlabDeployment, } from '@brandlab-digital/site-core/site';
|
|
3
|
-
import { buildBrandlabHeaderRules, } from './headers.js';
|
|
5
|
+
import { buildBrandlabHeaderRules, parseBrandlabStaticAssetCache, } from './headers.js';
|
|
4
6
|
/** Sharp's Linux binaries must travel with the contact route's image sanitiser. */
|
|
5
7
|
export const BRANDLAB_CONTACT_OUTPUT_FILE_TRACING = Object.freeze({
|
|
6
8
|
'/api/contact': Object.freeze([
|
|
@@ -18,8 +20,10 @@ const OPTION_KEYS = new Set([
|
|
|
18
20
|
'hsts',
|
|
19
21
|
'permissionsPolicy',
|
|
20
22
|
'extraHeaders',
|
|
23
|
+
'staticAssetCache',
|
|
21
24
|
'mdx',
|
|
22
25
|
'environment',
|
|
26
|
+
'projectRoot',
|
|
23
27
|
]);
|
|
24
28
|
const IMAGE_KEYS = new Set(['deviceSizes', 'imageSizes', 'qualities', 'minimumCacheTTL', 'remotePatterns']);
|
|
25
29
|
function assertKnownKeys(value, allowed, field, hint = '') {
|
|
@@ -67,6 +71,71 @@ function buildImagesConfig(images) {
|
|
|
67
71
|
: {}),
|
|
68
72
|
};
|
|
69
73
|
}
|
|
74
|
+
const PAGE_OR_ROUTE_FILE = /^(?:page|route)\.[A-Za-z0-9]+$/;
|
|
75
|
+
const METADATA_ROUTE_FILE = /^(?:opengraph-image|twitter-image|icon|apple-icon|sitemap|robots|manifest)(?:[\w.-]*)\.[A-Za-z0-9]+$/;
|
|
76
|
+
function isDirectory(path) {
|
|
77
|
+
try {
|
|
78
|
+
return statSync(path).isDirectory();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The first route file under an app directory that can answer a path strictly
|
|
86
|
+
* below `/<folder>/`. `depth` counts the folder segments matched so far: route
|
|
87
|
+
* groups, intercepts and parallel slots add none, a dynamic segment matches any
|
|
88
|
+
* one, and a catch-all reaches every path below the folder.
|
|
89
|
+
*/
|
|
90
|
+
function routeBelowFolder(directory, folder, depth, relative) {
|
|
91
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
92
|
+
const name = entry.name;
|
|
93
|
+
if (name.startsWith('_') || name.startsWith('.') || name === 'node_modules')
|
|
94
|
+
continue;
|
|
95
|
+
const path = join(directory, name);
|
|
96
|
+
const nextRelative = posix.join(relative, name);
|
|
97
|
+
if (entry.isFile()) {
|
|
98
|
+
// Page and route files serve their own directory; metadata files serve a path below it.
|
|
99
|
+
const below = depth > folder.length && PAGE_OR_ROUTE_FILE.test(name);
|
|
100
|
+
const metadata = depth >= folder.length && METADATA_ROUTE_FILE.test(name);
|
|
101
|
+
if (below || metadata)
|
|
102
|
+
return nextRelative;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (!entry.isDirectory())
|
|
106
|
+
continue;
|
|
107
|
+
let next = null;
|
|
108
|
+
if (name.startsWith('(') || name.startsWith('@'))
|
|
109
|
+
next = depth;
|
|
110
|
+
else if (/^\[\[?\.\.\./.test(name))
|
|
111
|
+
next = folder.length + 1;
|
|
112
|
+
else if (depth >= folder.length || name.startsWith('[') || name === folder[depth])
|
|
113
|
+
next = depth + 1;
|
|
114
|
+
if (next === null)
|
|
115
|
+
continue;
|
|
116
|
+
const found = routeBelowFolder(path, folder, next, nextRelative);
|
|
117
|
+
if (found)
|
|
118
|
+
return found;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
/** Each declared folder exists under public/ and no app route answers below it. */
|
|
123
|
+
function assertStaticAssetFolders(folders, root) {
|
|
124
|
+
for (const { source, folder } of folders) {
|
|
125
|
+
const publicFolder = posix.join('public', ...folder);
|
|
126
|
+
if (!isDirectory(join(root, publicFolder))) {
|
|
127
|
+
throw new Error(`staticAssetCache "${source}": ${publicFolder} is not a folder of static files in ${root}.`);
|
|
128
|
+
}
|
|
129
|
+
for (const appDirectory of ['src/app', 'app']) {
|
|
130
|
+
if (!isDirectory(join(root, appDirectory)))
|
|
131
|
+
continue;
|
|
132
|
+
const route = routeBelowFolder(join(root, appDirectory), folder, 0, appDirectory);
|
|
133
|
+
if (route) {
|
|
134
|
+
throw new Error(`staticAssetCache "${source}": ${route} answers below /${folder.join('/')}, so immutable caching could reach a page or route.`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
70
139
|
/**
|
|
71
140
|
* Converts the redirect table to Next redirects: each entry keeps its 301 or
|
|
72
141
|
* 308 status (Next forwards the query string), gains a single-hop
|
|
@@ -114,12 +183,16 @@ export function defineBrandlabNextConfig(options) {
|
|
|
114
183
|
throw new TypeError('mdx must be the @next/mdx config wrapper.');
|
|
115
184
|
}
|
|
116
185
|
const deployment = resolveBrandlabDeployment(options.environment ?? process.env);
|
|
186
|
+
if (options.staticAssetCache !== undefined) {
|
|
187
|
+
assertStaticAssetFolders(parseBrandlabStaticAssetCache(options.staticAssetCache), options.projectRoot ?? process.cwd());
|
|
188
|
+
}
|
|
117
189
|
const headerRules = buildBrandlabHeaderRules({
|
|
118
190
|
deployment,
|
|
119
191
|
...(options.csp ? { contentSecurityPolicyExtensions: options.csp } : {}),
|
|
120
192
|
...(options.hsts ? { hsts: options.hsts } : {}),
|
|
121
193
|
...(options.permissionsPolicy ? { permissionsPolicy: options.permissionsPolicy } : {}),
|
|
122
194
|
...(options.extraHeaders ? { extraHeaders: options.extraHeaders } : {}),
|
|
195
|
+
...(options.staticAssetCache ? { staticAssetCache: options.staticAssetCache } : {}),
|
|
123
196
|
});
|
|
124
197
|
const redirects = buildBrandlabRedirects({ site: options.site, redirects: options.redirects });
|
|
125
198
|
const config = {
|
package/dist/next-config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"next-config.js","sourceRoot":"","sources":["../src/next-config.ts"],"names":[],"mappings":"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","sourcesContent":["import { defineRedirects, type BrandlabRedirectTable } from '@brandlab-digital/site-core/redirects'\nimport {\n resolveBrandlabDeployment,\n type BrandlabDeploymentEnvironment,\n type BrandlabEnvironment,\n type BrandlabSite,\n} from '@brandlab-digital/site-core/site'\nimport {\n buildBrandlabHeaderRules,\n type BrandlabContentSecurityPolicyExtensions,\n type BrandlabHeader,\n type BrandlabHeaderRule,\n type BrandlabHstsExpansion,\n} from './headers.js'\n\n/** Next redirect records: 308 stays `permanent: true`, 301 stays `statusCode: 301`. */\nexport type BrandlabNextRedirect = {\n source: string\n destination: string\n has?: { type: 'host'; value: string }[]\n} & ({ permanent: true; statusCode?: never } | { statusCode: 301; permanent?: never })\n\nexport interface BrandlabNextRemotePattern {\n protocol?: 'http' | 'https'\n hostname: string\n port?: string\n pathname?: string\n search?: string\n}\n\n/** The site's own image settings; the output format is always WebP. */\nexport interface BrandlabNextImagesOptions {\n deviceSizes?: readonly number[]\n imageSizes?: readonly number[]\n qualities?: readonly number[]\n minimumCacheTTL?: number\n remotePatterns?: readonly BrandlabNextRemotePattern[]\n}\n\nexport interface BrandlabNextImagesConfig {\n formats: 'image/webp'[]\n deviceSizes?: number[]\n imageSizes?: number[]\n qualities?: number[]\n minimumCacheTTL?: number\n remotePatterns?: BrandlabNextRemotePattern[]\n}\n\nexport interface BrandlabNextConfig {\n poweredByHeader: false\n /** Trailing slashes are handled by single-hop redirect rules instead. */\n skipTrailingSlashRedirect: true\n env: { NEXT_PUBLIC_VERCEL_ENV: BrandlabDeploymentEnvironment }\n outputFileTracingIncludes: Record<string, string[]>\n images: BrandlabNextImagesConfig\n pageExtensions?: string[]\n headers: () => Promise<BrandlabHeaderRule[]>\n redirects: () => Promise<BrandlabNextRedirect[]>\n}\n\nexport interface BrandlabNextConfigOptions {\n site: BrandlabSite\n /** The src/data/redirects.ts table (defineRedirects). */\n redirects: BrandlabRedirectTable\n images: BrandlabNextImagesOptions\n /** Reviewed CSP origins for declared integrations such as Meta Pixel or map embeds. */\n csp?: BrandlabContentSecurityPolicyExtensions\n hsts?: BrandlabHstsExpansion\n /** Reviewed exception to the default Permissions-Policy that is already live. */\n permissionsPolicy?: string\n /** Reviewed response headers that are already live; protected headers cannot be replaced. */\n extraHeaders?: readonly BrandlabHeader[]\n /** Defaults to process.env; tests supply a fixed environment. */\n environment?: BrandlabEnvironment\n}\n\n/** Sharp's Linux binaries must travel with the contact route's image sanitiser. */\nexport const BRANDLAB_CONTACT_OUTPUT_FILE_TRACING = Object.freeze({\n '/api/contact': Object.freeze([\n 'node_modules/sharp/**/*',\n 'node_modules/@img/sharp-linux-x64/**/*',\n 'node_modules/@img/sharp-libvips-linux-x64/**/*',\n ]),\n})\n\nconst MDX_PAGE_EXTENSIONS = ['tsx', 'ts', 'jsx', 'js', 'mdx', 'md']\nconst OPTION_KEYS = new Set([\n 'site',\n 'redirects',\n 'images',\n 'csp',\n 'hsts',\n 'permissionsPolicy',\n 'extraHeaders',\n 'mdx',\n 'environment',\n])\nconst IMAGE_KEYS = new Set(['deviceSizes', 'imageSizes', 'qualities', 'minimumCacheTTL', 'remotePatterns'])\n\nfunction assertKnownKeys(value: object, allowed: ReadonlySet<string>, field: string, hint = '') {\n const unsupported = Object.keys(value).filter((key) => !allowed.has(key))\n if (unsupported.length > 0) {\n throw new Error(`${field} contains unsupported keys: ${unsupported.join(', ')}.${hint}`)\n }\n}\n\nfunction positiveIntegers(values: readonly number[] | undefined, field: string) {\n if (values === undefined) return undefined\n if (\n !Array.isArray(values) ||\n values.length === 0 ||\n values.some((value) => !Number.isSafeInteger(value) || value < 1)\n ) {\n throw new Error(`images.${field} must list positive integers.`)\n }\n return [...values]\n}\n\nfunction buildImagesConfig(images: BrandlabNextImagesOptions): BrandlabNextImagesConfig {\n if (!images || typeof images !== 'object' || Array.isArray(images)) {\n throw new Error('images must declare the site image settings ({} for Next defaults).')\n }\n assertKnownKeys(images, IMAGE_KEYS, 'images', ' The output format is always image/webp.')\n const deviceSizes = positiveIntegers(images.deviceSizes, 'deviceSizes')\n const imageSizes = positiveIntegers(images.imageSizes, 'imageSizes')\n const qualities = positiveIntegers(images.qualities, 'qualities')\n if (qualities?.some((quality) => quality > 100)) {\n throw new Error('images.qualities must be between 1 and 100.')\n }\n if (\n images.minimumCacheTTL !== undefined &&\n (!Number.isSafeInteger(images.minimumCacheTTL) || images.minimumCacheTTL < 0)\n ) {\n throw new Error('images.minimumCacheTTL must be a non-negative integer of seconds.')\n }\n if (images.remotePatterns !== undefined && !Array.isArray(images.remotePatterns)) {\n throw new Error('images.remotePatterns must be an array.')\n }\n return {\n formats: ['image/webp'],\n ...(deviceSizes ? { deviceSizes } : {}),\n ...(imageSizes ? { imageSizes } : {}),\n ...(qualities ? { qualities } : {}),\n ...(images.minimumCacheTTL !== undefined ? { minimumCacheTTL: images.minimumCacheTTL } : {}),\n ...(images.remotePatterns\n ? { remotePatterns: images.remotePatterns.map((pattern) => ({ ...pattern })) }\n : {}),\n }\n}\n\n/**\n * Converts the redirect table to Next redirects: each entry keeps its 301 or\n * 308 status (Next forwards the query string), gains a single-hop\n * trailing-slash variant, then one trailing-slash rule and one permanent\n * canonical-host rule per alias host follow. There is no x-forwarded-proto\n * rule: Vercel's edge already enforces HTTPS.\n */\nexport function buildBrandlabRedirects(options: {\n site: BrandlabSite\n redirects: BrandlabRedirectTable\n}): BrandlabNextRedirect[] {\n const table = defineRedirects(options.redirects)\n const entries = table.flatMap((redirect): BrandlabNextRedirect[] => {\n const status = redirect.status === 301\n ? { statusCode: 301 as const }\n : { permanent: true as const }\n const has = redirect.host ? { has: [{ type: 'host' as const, value: redirect.host }] } : {}\n const record = (source: string): BrandlabNextRedirect => ({\n source,\n destination: redirect.destination,\n ...status,\n ...has,\n })\n return redirect.source === '/'\n ? [record(redirect.source)]\n : [record(redirect.source), record(`${redirect.source}/`)]\n })\n return [\n ...entries,\n { source: '/:path+/', destination: '/:path+', permanent: true },\n ...options.site.aliasHosts.map((host): BrandlabNextRedirect => ({\n source: '/:path*',\n has: [{ type: 'host', value: host }],\n destination: `${options.site.canonicalUrl}/:path*`,\n permanent: true,\n })),\n ]\n}\n\n/**\n * The whole next.config.ts after its first line\n * `import '@brandlab-digital/site-next/image-runtime'`:\n * `export default defineBrandlabNextConfig({ site, redirects, images })`.\n *\n * Sets poweredByHeader: false, WebP image output, Sharp output tracing for\n * /api/contact, NEXT_PUBLIC_VERCEL_ENV for browser gates, the deployment's\n * header rules and the redirect table. Validation runs when the config loads,\n * so an invalid table or header exception fails the build.\n */\nexport function defineBrandlabNextConfig(\n options: BrandlabNextConfigOptions & { mdx?: undefined },\n): BrandlabNextConfig\nexport function defineBrandlabNextConfig<Wrapped>(\n options: BrandlabNextConfigOptions & {\n /** The @next/mdx wrapper: createMDX({ options: { remarkPlugins: ['remark-gfm'] } }). */\n mdx: (config: BrandlabNextConfig) => Wrapped\n },\n): Wrapped\nexport function defineBrandlabNextConfig(\n options: BrandlabNextConfigOptions & { mdx?: (config: BrandlabNextConfig) => unknown },\n): unknown {\n if (!options || typeof options !== 'object') {\n throw new TypeError('defineBrandlabNextConfig requires the site, redirects and images.')\n }\n assertKnownKeys(options, OPTION_KEYS, 'next.config options')\n if (!options.site?.canonicalUrl || !Array.isArray(options.site.aliasHosts)) {\n throw new Error('site must come from defineBrandlabSite.')\n }\n if (options.mdx !== undefined && typeof options.mdx !== 'function') {\n throw new TypeError('mdx must be the @next/mdx config wrapper.')\n }\n const deployment = resolveBrandlabDeployment(options.environment ?? process.env)\n const headerRules = buildBrandlabHeaderRules({\n deployment,\n ...(options.csp ? { contentSecurityPolicyExtensions: options.csp } : {}),\n ...(options.hsts ? { hsts: options.hsts } : {}),\n ...(options.permissionsPolicy ? { permissionsPolicy: options.permissionsPolicy } : {}),\n ...(options.extraHeaders ? { extraHeaders: options.extraHeaders } : {}),\n })\n const redirects = buildBrandlabRedirects({ site: options.site, redirects: options.redirects })\n\n const config: BrandlabNextConfig = {\n poweredByHeader: false,\n skipTrailingSlashRedirect: true,\n env: { NEXT_PUBLIC_VERCEL_ENV: deployment.environment },\n outputFileTracingIncludes: Object.fromEntries(\n Object.entries(BRANDLAB_CONTACT_OUTPUT_FILE_TRACING).map(([route, globs]) => [route, [...globs]]),\n ),\n images: buildImagesConfig(options.images),\n ...(options.mdx ? { pageExtensions: [...MDX_PAGE_EXTENSIONS] } : {}),\n headers: async () =>\n headerRules.map((rule) => ({ source: rule.source, headers: rule.headers.map((header) => ({ ...header })) })),\n redirects: async () =>\n redirects.map((redirect) => ({\n ...redirect,\n ...(redirect.has ? { has: redirect.has.map((condition) => ({ ...condition })) } : {}),\n })),\n }\n return options.mdx ? options.mdx(config) : config\n}\n"]}
|
|
1
|
+
{"version":3,"file":"next-config.js","sourceRoot":"","sources":["../src/next-config.ts"],"names":[],"mappings":"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","sourcesContent":["import { readdirSync, statSync } from 'node:fs'\nimport { join, posix } from 'node:path'\nimport { defineRedirects, type BrandlabRedirectTable } from '@brandlab-digital/site-core/redirects'\nimport {\n resolveBrandlabDeployment,\n type BrandlabDeploymentEnvironment,\n type BrandlabEnvironment,\n type BrandlabSite,\n} from '@brandlab-digital/site-core/site'\nimport {\n buildBrandlabHeaderRules,\n parseBrandlabStaticAssetCache,\n type BrandlabContentSecurityPolicyExtensions,\n type BrandlabHeader,\n type BrandlabHeaderRule,\n type BrandlabHstsExpansion,\n type BrandlabStaticAssetFolder,\n} from './headers.js'\n\n/** Next redirect records: 308 stays `permanent: true`, 301 stays `statusCode: 301`. */\nexport type BrandlabNextRedirect = {\n source: string\n destination: string\n has?: { type: 'host'; value: string }[]\n} & ({ permanent: true; statusCode?: never } | { statusCode: 301; permanent?: never })\n\nexport interface BrandlabNextRemotePattern {\n protocol?: 'http' | 'https'\n hostname: string\n port?: string\n pathname?: string\n search?: string\n}\n\n/** The site's own image settings; the output format is always WebP. */\nexport interface BrandlabNextImagesOptions {\n deviceSizes?: readonly number[]\n imageSizes?: readonly number[]\n qualities?: readonly number[]\n minimumCacheTTL?: number\n remotePatterns?: readonly BrandlabNextRemotePattern[]\n}\n\nexport interface BrandlabNextImagesConfig {\n formats: 'image/webp'[]\n deviceSizes?: number[]\n imageSizes?: number[]\n qualities?: number[]\n minimumCacheTTL?: number\n remotePatterns?: BrandlabNextRemotePattern[]\n}\n\nexport interface BrandlabNextConfig {\n poweredByHeader: false\n /** Trailing slashes are handled by single-hop redirect rules instead. */\n skipTrailingSlashRedirect: true\n env: { NEXT_PUBLIC_VERCEL_ENV: BrandlabDeploymentEnvironment }\n outputFileTracingIncludes: Record<string, string[]>\n images: BrandlabNextImagesConfig\n pageExtensions?: string[]\n headers: () => Promise<BrandlabHeaderRule[]>\n redirects: () => Promise<BrandlabNextRedirect[]>\n}\n\nexport interface BrandlabNextConfigOptions {\n site: BrandlabSite\n /** The src/data/redirects.ts table (defineRedirects). */\n redirects: BrandlabRedirectTable\n images: BrandlabNextImagesOptions\n /** Reviewed CSP origins for declared integrations such as Meta Pixel or map embeds. */\n csp?: BrandlabContentSecurityPolicyExtensions\n hsts?: BrandlabHstsExpansion\n /** Reviewed exception to the default Permissions-Policy that is already live. */\n permissionsPolicy?: string\n /** Reviewed response headers that are already live; protected headers cannot be replaced. */\n extraHeaders?: readonly BrandlabHeader[]\n /**\n * Public folders of static files that never change in place, written as Next\n * sources such as `['/images/:path*', '/fonts/:path*']`. Production builds serve\n * their files (names with an extension) with `public, max-age=31536000, immutable`;\n * Previews keep `private, no-store`. Each folder must exist under `public/`, and\n * no route under `src/app` or `app` may answer below it.\n */\n staticAssetCache?: readonly string[]\n /** Defaults to process.env; tests supply a fixed environment. */\n environment?: BrandlabEnvironment\n /** The project root for the staticAssetCache checks; defaults to process.cwd(). */\n projectRoot?: string\n}\n\n/** Sharp's Linux binaries must travel with the contact route's image sanitiser. */\nexport const BRANDLAB_CONTACT_OUTPUT_FILE_TRACING = Object.freeze({\n '/api/contact': Object.freeze([\n 'node_modules/sharp/**/*',\n 'node_modules/@img/sharp-linux-x64/**/*',\n 'node_modules/@img/sharp-libvips-linux-x64/**/*',\n ]),\n})\n\nconst MDX_PAGE_EXTENSIONS = ['tsx', 'ts', 'jsx', 'js', 'mdx', 'md']\nconst OPTION_KEYS = new Set([\n 'site',\n 'redirects',\n 'images',\n 'csp',\n 'hsts',\n 'permissionsPolicy',\n 'extraHeaders',\n 'staticAssetCache',\n 'mdx',\n 'environment',\n 'projectRoot',\n])\nconst IMAGE_KEYS = new Set(['deviceSizes', 'imageSizes', 'qualities', 'minimumCacheTTL', 'remotePatterns'])\n\nfunction assertKnownKeys(value: object, allowed: ReadonlySet<string>, field: string, hint = '') {\n const unsupported = Object.keys(value).filter((key) => !allowed.has(key))\n if (unsupported.length > 0) {\n throw new Error(`${field} contains unsupported keys: ${unsupported.join(', ')}.${hint}`)\n }\n}\n\nfunction positiveIntegers(values: readonly number[] | undefined, field: string) {\n if (values === undefined) return undefined\n if (\n !Array.isArray(values) ||\n values.length === 0 ||\n values.some((value) => !Number.isSafeInteger(value) || value < 1)\n ) {\n throw new Error(`images.${field} must list positive integers.`)\n }\n return [...values]\n}\n\nfunction buildImagesConfig(images: BrandlabNextImagesOptions): BrandlabNextImagesConfig {\n if (!images || typeof images !== 'object' || Array.isArray(images)) {\n throw new Error('images must declare the site image settings ({} for Next defaults).')\n }\n assertKnownKeys(images, IMAGE_KEYS, 'images', ' The output format is always image/webp.')\n const deviceSizes = positiveIntegers(images.deviceSizes, 'deviceSizes')\n const imageSizes = positiveIntegers(images.imageSizes, 'imageSizes')\n const qualities = positiveIntegers(images.qualities, 'qualities')\n if (qualities?.some((quality) => quality > 100)) {\n throw new Error('images.qualities must be between 1 and 100.')\n }\n if (\n images.minimumCacheTTL !== undefined &&\n (!Number.isSafeInteger(images.minimumCacheTTL) || images.minimumCacheTTL < 0)\n ) {\n throw new Error('images.minimumCacheTTL must be a non-negative integer of seconds.')\n }\n if (images.remotePatterns !== undefined && !Array.isArray(images.remotePatterns)) {\n throw new Error('images.remotePatterns must be an array.')\n }\n return {\n formats: ['image/webp'],\n ...(deviceSizes ? { deviceSizes } : {}),\n ...(imageSizes ? { imageSizes } : {}),\n ...(qualities ? { qualities } : {}),\n ...(images.minimumCacheTTL !== undefined ? { minimumCacheTTL: images.minimumCacheTTL } : {}),\n ...(images.remotePatterns\n ? { remotePatterns: images.remotePatterns.map((pattern) => ({ ...pattern })) }\n : {}),\n }\n}\n\nconst PAGE_OR_ROUTE_FILE = /^(?:page|route)\\.[A-Za-z0-9]+$/\nconst METADATA_ROUTE_FILE = /^(?:opengraph-image|twitter-image|icon|apple-icon|sitemap|robots|manifest)(?:[\\w.-]*)\\.[A-Za-z0-9]+$/\n\nfunction isDirectory(path: string) {\n try {\n return statSync(path).isDirectory()\n } catch {\n return false\n }\n}\n\n/**\n * The first route file under an app directory that can answer a path strictly\n * below `/<folder>/`. `depth` counts the folder segments matched so far: route\n * groups, intercepts and parallel slots add none, a dynamic segment matches any\n * one, and a catch-all reaches every path below the folder.\n */\nfunction routeBelowFolder(\n directory: string,\n folder: readonly string[],\n depth: number,\n relative: string,\n): string | null {\n for (const entry of readdirSync(directory, { withFileTypes: true })) {\n const name = entry.name\n if (name.startsWith('_') || name.startsWith('.') || name === 'node_modules') continue\n const path = join(directory, name)\n const nextRelative = posix.join(relative, name)\n if (entry.isFile()) {\n // Page and route files serve their own directory; metadata files serve a path below it.\n const below = depth > folder.length && PAGE_OR_ROUTE_FILE.test(name)\n const metadata = depth >= folder.length && METADATA_ROUTE_FILE.test(name)\n if (below || metadata) return nextRelative\n continue\n }\n if (!entry.isDirectory()) continue\n let next: number | null = null\n if (name.startsWith('(') || name.startsWith('@')) next = depth\n else if (/^\\[\\[?\\.\\.\\./.test(name)) next = folder.length + 1\n else if (depth >= folder.length || name.startsWith('[') || name === folder[depth]) next = depth + 1\n if (next === null) continue\n const found = routeBelowFolder(path, folder, next, nextRelative)\n if (found) return found\n }\n return null\n}\n\n/** Each declared folder exists under public/ and no app route answers below it. */\nfunction assertStaticAssetFolders(folders: readonly BrandlabStaticAssetFolder[], root: string) {\n for (const { source, folder } of folders) {\n const publicFolder = posix.join('public', ...folder)\n if (!isDirectory(join(root, publicFolder))) {\n throw new Error(`staticAssetCache \"${source}\": ${publicFolder} is not a folder of static files in ${root}.`)\n }\n for (const appDirectory of ['src/app', 'app']) {\n if (!isDirectory(join(root, appDirectory))) continue\n const route = routeBelowFolder(join(root, appDirectory), folder, 0, appDirectory)\n if (route) {\n throw new Error(\n `staticAssetCache \"${source}\": ${route} answers below /${folder.join('/')}, so immutable caching could reach a page or route.`,\n )\n }\n }\n }\n}\n\n/**\n * Converts the redirect table to Next redirects: each entry keeps its 301 or\n * 308 status (Next forwards the query string), gains a single-hop\n * trailing-slash variant, then one trailing-slash rule and one permanent\n * canonical-host rule per alias host follow. There is no x-forwarded-proto\n * rule: Vercel's edge already enforces HTTPS.\n */\nexport function buildBrandlabRedirects(options: {\n site: BrandlabSite\n redirects: BrandlabRedirectTable\n}): BrandlabNextRedirect[] {\n const table = defineRedirects(options.redirects)\n const entries = table.flatMap((redirect): BrandlabNextRedirect[] => {\n const status = redirect.status === 301\n ? { statusCode: 301 as const }\n : { permanent: true as const }\n const has = redirect.host ? { has: [{ type: 'host' as const, value: redirect.host }] } : {}\n const record = (source: string): BrandlabNextRedirect => ({\n source,\n destination: redirect.destination,\n ...status,\n ...has,\n })\n return redirect.source === '/'\n ? [record(redirect.source)]\n : [record(redirect.source), record(`${redirect.source}/`)]\n })\n return [\n ...entries,\n { source: '/:path+/', destination: '/:path+', permanent: true },\n ...options.site.aliasHosts.map((host): BrandlabNextRedirect => ({\n source: '/:path*',\n has: [{ type: 'host', value: host }],\n destination: `${options.site.canonicalUrl}/:path*`,\n permanent: true,\n })),\n ]\n}\n\n/**\n * The whole next.config.ts after its first line\n * `import '@brandlab-digital/site-next/image-runtime'`:\n * `export default defineBrandlabNextConfig({ site, redirects, images })`.\n *\n * Sets poweredByHeader: false, WebP image output, Sharp output tracing for\n * /api/contact, NEXT_PUBLIC_VERCEL_ENV for browser gates, the deployment's\n * header rules (with immutable caching for the declared static public folders on\n * production builds) and the redirect table. Validation runs when the config\n * loads, so an invalid table, header exception or static folder fails the build.\n */\nexport function defineBrandlabNextConfig(\n options: BrandlabNextConfigOptions & { mdx?: undefined },\n): BrandlabNextConfig\nexport function defineBrandlabNextConfig<Wrapped>(\n options: BrandlabNextConfigOptions & {\n /** The @next/mdx wrapper: createMDX({ options: { remarkPlugins: ['remark-gfm'] } }). */\n mdx: (config: BrandlabNextConfig) => Wrapped\n },\n): Wrapped\nexport function defineBrandlabNextConfig(\n options: BrandlabNextConfigOptions & { mdx?: (config: BrandlabNextConfig) => unknown },\n): unknown {\n if (!options || typeof options !== 'object') {\n throw new TypeError('defineBrandlabNextConfig requires the site, redirects and images.')\n }\n assertKnownKeys(options, OPTION_KEYS, 'next.config options')\n if (!options.site?.canonicalUrl || !Array.isArray(options.site.aliasHosts)) {\n throw new Error('site must come from defineBrandlabSite.')\n }\n if (options.mdx !== undefined && typeof options.mdx !== 'function') {\n throw new TypeError('mdx must be the @next/mdx config wrapper.')\n }\n const deployment = resolveBrandlabDeployment(options.environment ?? process.env)\n if (options.staticAssetCache !== undefined) {\n assertStaticAssetFolders(\n parseBrandlabStaticAssetCache(options.staticAssetCache),\n options.projectRoot ?? process.cwd(),\n )\n }\n const headerRules = buildBrandlabHeaderRules({\n deployment,\n ...(options.csp ? { contentSecurityPolicyExtensions: options.csp } : {}),\n ...(options.hsts ? { hsts: options.hsts } : {}),\n ...(options.permissionsPolicy ? { permissionsPolicy: options.permissionsPolicy } : {}),\n ...(options.extraHeaders ? { extraHeaders: options.extraHeaders } : {}),\n ...(options.staticAssetCache ? { staticAssetCache: options.staticAssetCache } : {}),\n })\n const redirects = buildBrandlabRedirects({ site: options.site, redirects: options.redirects })\n\n const config: BrandlabNextConfig = {\n poweredByHeader: false,\n skipTrailingSlashRedirect: true,\n env: { NEXT_PUBLIC_VERCEL_ENV: deployment.environment },\n outputFileTracingIncludes: Object.fromEntries(\n Object.entries(BRANDLAB_CONTACT_OUTPUT_FILE_TRACING).map(([route, globs]) => [route, [...globs]]),\n ),\n images: buildImagesConfig(options.images),\n ...(options.mdx ? { pageExtensions: [...MDX_PAGE_EXTENSIONS] } : {}),\n headers: async () =>\n headerRules.map((rule) => ({ source: rule.source, headers: rule.headers.map((header) => ({ ...header })) })),\n redirects: async () =>\n redirects.map((redirect) => ({\n ...redirect,\n ...(redirect.has ? { has: redirect.has.map((condition) => ({ ...condition })) } : {}),\n })),\n }\n return options.mdx ? options.mdx(config) : config\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brandlab-digital/site-next",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/RomieDandan/brandlab.git",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@brandlab-digital/site-core": "3.
|
|
23
|
+
"@brandlab-digital/site-core": "3.1.0",
|
|
24
24
|
"browser-image-compression": "2.0.2",
|
|
25
25
|
"pdf-lib": "1.17.1",
|
|
26
26
|
"server-only": "0.0.1",
|