@bygghammar/cms-platform 0.57.0 → 0.58.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/CHANGELOG.md +11 -0
- package/package.json +6 -1
- package/src/admin/version-core.ts +76 -0
- package/src/config/package-owned-modules.ts +63 -0
- package/src/server/schemas.ts +11 -11
- package/src/site/font-bundle.ts +43 -0
- package/src/site/page-context.ts +87 -0
- package/src/site/public-origin.ts +47 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.58.0
|
|
4
|
+
|
|
5
|
+
- Utilities sweep: the public-page context loader (`site/page-context`), the
|
|
6
|
+
deployment origin resolver (`site/public-origin`), the self-hosted font bundle
|
|
7
|
+
(`site/font-bundle`) and the version screen's build/release metadata
|
|
8
|
+
(`admin/version-core`) are now package-owned factories. Sites pass their own
|
|
9
|
+
data sources, compiled origin and font assets.
|
|
10
|
+
- New drift guard: `config/package-owned-modules` lists every module a site must
|
|
11
|
+
keep as a thin wrapper, so a site test fails the build if platform behaviour is
|
|
12
|
+
reimplemented locally.
|
|
13
|
+
|
|
3
14
|
## 0.57.0
|
|
4
15
|
|
|
5
16
|
- Server-function inputs are now platform contracts: `server/schemas` ships the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bygghammar/cms-platform",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.58.0",
|
|
4
4
|
"description": "Shared CMS platform: blocks, block types, i18n core, UI primitives and design-token contract used by every Bygghammar/Voyara site.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -172,6 +172,11 @@
|
|
|
172
172
|
"./backoffice/ResetPasswordView": "./src/backoffice/ResetPasswordView.tsx",
|
|
173
173
|
"./backoffice/routes/pages": "./src/backoffice/routes/pages.tsx",
|
|
174
174
|
"./config/define-site": "./src/config/define-site.ts",
|
|
175
|
+
"./site/page-context": "./src/site/page-context.ts",
|
|
176
|
+
"./site/public-origin": "./src/site/public-origin.ts",
|
|
177
|
+
"./site/font-bundle": "./src/site/font-bundle.ts",
|
|
178
|
+
"./admin/version-core": "./src/admin/version-core.ts",
|
|
179
|
+
"./config/package-owned-modules": "./src/config/package-owned-modules.ts",
|
|
175
180
|
"./server/schema-drift": "./src/server/schema-drift.ts",
|
|
176
181
|
"./server/media-focus-core": "./src/server/media-focus-core.ts",
|
|
177
182
|
"./server/media-meta-core": "./src/server/media-meta-core.ts",
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build metadata and platform release history for the version screen.
|
|
3
|
+
*
|
|
4
|
+
* The build constants are injected by the site's Vite config (they must be, the
|
|
5
|
+
* values only exist at build time); reading them defensively and fetching the
|
|
6
|
+
* package's published releases is identical everywhere, so it lives here.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type BuildInfo = {
|
|
10
|
+
appVersion: string;
|
|
11
|
+
buildTime: string;
|
|
12
|
+
platformVersion: string;
|
|
13
|
+
platformRange: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type PlatformRelease = {
|
|
17
|
+
version: string;
|
|
18
|
+
publishedAt: string | null;
|
|
19
|
+
current: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Raw build constants as provided by the host build. */
|
|
23
|
+
export type BuildConstants = {
|
|
24
|
+
appVersion?: unknown;
|
|
25
|
+
buildTime?: unknown;
|
|
26
|
+
platformVersion?: unknown;
|
|
27
|
+
platformRange?: unknown;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const text = (value: unknown, fallback: string): string =>
|
|
31
|
+
typeof value === "string" && value ? value : fallback;
|
|
32
|
+
|
|
33
|
+
/** Normalize the injected build constants into a complete `BuildInfo`. */
|
|
34
|
+
export function readBuildInfoFrom(constants: BuildConstants): BuildInfo {
|
|
35
|
+
return {
|
|
36
|
+
appVersion: text(constants.appVersion, "0.0.0"),
|
|
37
|
+
buildTime: text(constants.buildTime, new Date().toISOString()),
|
|
38
|
+
platformVersion: text(constants.platformVersion, "unknown"),
|
|
39
|
+
platformRange: text(constants.platformRange, "unknown"),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The npm package name of the shared CMS platform. */
|
|
44
|
+
export const PLATFORM_PACKAGE = "@bygghammar/cms-platform";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The most recent published platform releases, newest first.
|
|
48
|
+
*
|
|
49
|
+
* Network failures are not an error worth surfacing on an information screen —
|
|
50
|
+
* the screen still shows the running version from the build constants.
|
|
51
|
+
*/
|
|
52
|
+
export async function fetchPlatformReleases(
|
|
53
|
+
currentVersion: string,
|
|
54
|
+
limit = 3,
|
|
55
|
+
): Promise<PlatformRelease[]> {
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch(
|
|
58
|
+
`https://registry.npmjs.org/${PLATFORM_PACKAGE.replace("/", "%2F")}`,
|
|
59
|
+
{ headers: { accept: "application/json" } },
|
|
60
|
+
);
|
|
61
|
+
if (!res.ok) return [];
|
|
62
|
+
const json = (await res.json()) as { time?: Record<string, string> };
|
|
63
|
+
const time = json.time ?? {};
|
|
64
|
+
return Object.entries(time)
|
|
65
|
+
.filter(([v]) => v !== "created" && v !== "modified")
|
|
66
|
+
.map(([version, publishedAt]) => ({
|
|
67
|
+
version,
|
|
68
|
+
publishedAt,
|
|
69
|
+
current: version === currentVersion,
|
|
70
|
+
}))
|
|
71
|
+
.sort((a, b) => (a.publishedAt < b.publishedAt ? 1 : -1))
|
|
72
|
+
.slice(0, limit);
|
|
73
|
+
} catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drift guard manifest.
|
|
3
|
+
*
|
|
4
|
+
* Each entry names a module a site is expected to keep as a thin wrapper, and
|
|
5
|
+
* the package export it must delegate to. A site test walks this manifest and
|
|
6
|
+
* fails the build when a local file stops importing the package module — the
|
|
7
|
+
* failure mode this whole extraction exists to prevent, where a site quietly
|
|
8
|
+
* reimplements platform behaviour and then drifts away from every other site.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type OwnedModule = {
|
|
12
|
+
/** Path relative to the site's project root. */
|
|
13
|
+
site: string;
|
|
14
|
+
/** Package specifier the site file must reference. */
|
|
15
|
+
packageModule: string;
|
|
16
|
+
/** Whether the site is allowed not to have the file at all. */
|
|
17
|
+
optional?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const PACKAGE_OWNED_MODULES: OwnedModule[] = [
|
|
21
|
+
{ site: "src/lib/i18n/routes.ts", packageModule: "@bygghammar/cms-platform/site/routes" },
|
|
22
|
+
{
|
|
23
|
+
site: "src/lib/i18n/page-context.ts",
|
|
24
|
+
packageModule: "@bygghammar/cms-platform/site/page-context",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
site: "src/lib/site/public-origin.ts",
|
|
28
|
+
packageModule: "@bygghammar/cms-platform/site/public-origin",
|
|
29
|
+
},
|
|
30
|
+
{ site: "src/lib/site/site-url.ts", packageModule: "@bygghammar/cms-platform/site/site-url" },
|
|
31
|
+
{ site: "src/lib/fonts/bundle.ts", packageModule: "@bygghammar/cms-platform/site/font-bundle" },
|
|
32
|
+
{
|
|
33
|
+
site: "src/lib/admin/version.server.ts",
|
|
34
|
+
packageModule: "@bygghammar/cms-platform/admin/version-core",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
site: "src/lib/backup/backup.server.ts",
|
|
38
|
+
packageModule: "@bygghammar/cms-platform/server/backup-core",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
site: "src/components/site/SiteHeader.tsx",
|
|
42
|
+
packageModule: "@bygghammar/cms-platform/site/SiteHeader",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
site: "src/components/site/SiteFooter.tsx",
|
|
46
|
+
packageModule: "@bygghammar/cms-platform/site/SiteFooter",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
site: "src/components/site/PageRenderer.tsx",
|
|
50
|
+
packageModule: "@bygghammar/cms-platform/site/PageRenderer",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
site: "src/components/site/blocks/FormBlock.tsx",
|
|
54
|
+
packageModule: "@bygghammar/cms-platform/blocks/components/FormBlock",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
site: "src/components/site/blocks/CourseListBlock.tsx",
|
|
58
|
+
packageModule: "@bygghammar/cms-platform/blocks/components/TripListBlock",
|
|
59
|
+
optional: true,
|
|
60
|
+
},
|
|
61
|
+
{ site: "src/lib/blocks/registry.tsx", packageModule: "@bygghammar/cms-platform/blocks/registry" },
|
|
62
|
+
{ site: "src/lib/site/definition.ts", packageModule: "@bygghammar/cms-platform/config/define-site" },
|
|
63
|
+
];
|
package/src/server/schemas.ts
CHANGED
|
@@ -85,11 +85,11 @@ export const pageSchemas = {
|
|
|
85
85
|
* Administration inputs. Roles and provider kinds come from the platform's
|
|
86
86
|
* own enums, which the caller passes so a site cannot drift from them.
|
|
87
87
|
*/
|
|
88
|
-
export function adminSchemas(input: {
|
|
89
|
-
roles: readonly [
|
|
90
|
-
providerKinds: readonly [
|
|
88
|
+
export function adminSchemas<R extends string, K extends string>(input: {
|
|
89
|
+
roles: readonly [R, ...R[]];
|
|
90
|
+
providerKinds: readonly [K, ...K[]];
|
|
91
91
|
}) {
|
|
92
|
-
const role = z.enum(input.roles);
|
|
92
|
+
const role = z.enum(input.roles as unknown as [R, ...R[]]);
|
|
93
93
|
return {
|
|
94
94
|
role,
|
|
95
95
|
createUser: z.object({
|
|
@@ -114,7 +114,7 @@ export function adminSchemas(input: {
|
|
|
114
114
|
emailProvider: z.object({
|
|
115
115
|
id: uuid.optional(),
|
|
116
116
|
name: z.string().min(1).max(120),
|
|
117
|
-
kind: z.enum(input.providerKinds),
|
|
117
|
+
kind: z.enum(input.providerKinds as unknown as [K, ...K[]]),
|
|
118
118
|
is_active: z.boolean().default(true),
|
|
119
119
|
sort_order: z.number().int().min(0).max(9999).default(100),
|
|
120
120
|
from_name: z.string().max(200).nullable().optional(),
|
|
@@ -209,9 +209,9 @@ const localizedImage = z
|
|
|
209
209
|
* everything else — the localized string/image shape, the allowed Open Graph
|
|
210
210
|
* and Twitter values, the slug map — is a platform contract.
|
|
211
211
|
*/
|
|
212
|
-
export function pageSeoSchemas(input: {
|
|
213
|
-
pageKeys: readonly [
|
|
214
|
-
locales: readonly [
|
|
212
|
+
export function pageSeoSchemas<P extends string, L extends string>(input: {
|
|
213
|
+
pageKeys: readonly [P, ...P[]];
|
|
214
|
+
locales: readonly [L, ...L[]];
|
|
215
215
|
}) {
|
|
216
216
|
const seo = z
|
|
217
217
|
.object({
|
|
@@ -224,7 +224,7 @@ export function pageSeoSchemas(input: {
|
|
|
224
224
|
twitterCard: z.enum(["summary", "summary_large_image"]).optional(),
|
|
225
225
|
keywords: localizedString.optional(),
|
|
226
226
|
robots: z.string().max(120).optional(),
|
|
227
|
-
jsonLd: z.record(z.enum(input.locales), z.record(z.string(), z.unknown())).optional(),
|
|
227
|
+
jsonLd: z.record(z.enum(input.locales as unknown as [L, ...L[]]), z.record(z.string(), z.unknown())).optional(),
|
|
228
228
|
})
|
|
229
229
|
.strict();
|
|
230
230
|
|
|
@@ -233,8 +233,8 @@ export function pageSeoSchemas(input: {
|
|
|
233
233
|
return {
|
|
234
234
|
seo,
|
|
235
235
|
slugs,
|
|
236
|
-
save: z.object({ pageKey: z.enum(input.pageKeys), slugs, seo }),
|
|
237
|
-
delete: z.object({ pageKey: z.enum(input.pageKeys) }),
|
|
236
|
+
save: z.object({ pageKey: z.enum(input.pageKeys as unknown as [P, ...P[]]), slugs, seo }),
|
|
237
|
+
delete: z.object({ pageKey: z.enum(input.pageKeys as unknown as [P, ...P[]]) }),
|
|
238
238
|
} as const;
|
|
239
239
|
}
|
|
240
240
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-hosted WOFF2 payloads, bundled into the server build.
|
|
3
|
+
*
|
|
4
|
+
* Fonts deliberately do NOT live in `public/`: files served by the framework's
|
|
5
|
+
* static-asset handler bypass every application layer, so they come back without
|
|
6
|
+
* `Cache-Control` and without `Expires`. Serving them from a real route puts them
|
|
7
|
+
* back under the deployment's caching policy in both runtimes.
|
|
8
|
+
*
|
|
9
|
+
* The site imports its own `.woff2?inline` assets (they are brand assets) and
|
|
10
|
+
* hands the resulting data URIs to this factory; decoding and caching are shared.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type FontBundle = {
|
|
14
|
+
/** Every filename this bundle can serve. */
|
|
15
|
+
filenames: string[];
|
|
16
|
+
/** Font bytes, decoded once per process. `undefined` for unknown names. */
|
|
17
|
+
fontBytes: (name: string) => Uint8Array | undefined;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function fromDataUri(uri: string): Uint8Array {
|
|
21
|
+
const base64 = uri.slice(uri.indexOf(",") + 1);
|
|
22
|
+
const binary = atob(base64);
|
|
23
|
+
const bytes = new Uint8Array(binary.length);
|
|
24
|
+
for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
|
|
25
|
+
return bytes;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Build a font bundle from a `filename -> data URI` map. */
|
|
29
|
+
export function createFontBundle(dataUris: Record<string, string>): FontBundle {
|
|
30
|
+
const decoded = new Map<string, Uint8Array>();
|
|
31
|
+
return {
|
|
32
|
+
filenames: Object.keys(dataUris),
|
|
33
|
+
fontBytes(name: string): Uint8Array | undefined {
|
|
34
|
+
const cached = decoded.get(name);
|
|
35
|
+
if (cached) return cached;
|
|
36
|
+
const uri = dataUris[name];
|
|
37
|
+
if (!uri) return undefined;
|
|
38
|
+
const bytes = fromDataUri(uri);
|
|
39
|
+
decoded.set(name, bytes);
|
|
40
|
+
return bytes;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public-page context: the single server round-trip every public route needs.
|
|
3
|
+
*
|
|
4
|
+
* A public page needs four facts before it can render its `<head>`: the CMS SEO
|
|
5
|
+
* overrides, the locale implied by the request domain, the request origin, and
|
|
6
|
+
* the locale/domain map used for cross-domain hreflang. Fetching them one by one
|
|
7
|
+
* per route is what made this glue site-local; the engine is identical for every
|
|
8
|
+
* site, so it lives here and the site supplies only its data sources.
|
|
9
|
+
*/
|
|
10
|
+
import { queryOptions } from "@tanstack/react-query";
|
|
11
|
+
|
|
12
|
+
import type { Locale } from "../i18n/locales";
|
|
13
|
+
import type { PageHead, PageSeoOverrides, SiteRoutes } from "./routes";
|
|
14
|
+
|
|
15
|
+
/** One `locale -> domain` mapping row, as stored by the domains module. */
|
|
16
|
+
export type PageContextDomain = { locale: string; domain: string };
|
|
17
|
+
|
|
18
|
+
export type PageContext<Row = unknown> = {
|
|
19
|
+
seoRows: Row[];
|
|
20
|
+
locale: Locale;
|
|
21
|
+
origin: string;
|
|
22
|
+
domains: PageContextDomain[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type PageContextDeps<Row> = {
|
|
26
|
+
/** Raw `page_seo` rows for this site. */
|
|
27
|
+
getSeoRows: () => Promise<Row[]>;
|
|
28
|
+
/** Locale + origin derived from the incoming request. */
|
|
29
|
+
getRequestLocale: () => Promise<{ locale: Locale; origin: string }>;
|
|
30
|
+
/** Configured locale domains. */
|
|
31
|
+
getLocaleDomains: () => Promise<PageContextDomain[]>;
|
|
32
|
+
/** Convert stored rows into the routing engine's override shape. */
|
|
33
|
+
rowsToOverrides: (rows: Row[] | undefined) => PageSeoOverrides;
|
|
34
|
+
/** The site's routing engine (from `createSiteRoutes`). */
|
|
35
|
+
routes: Pick<SiteRoutes, "resolveRoute" | "pageHeadMeta">;
|
|
36
|
+
/** Locale used when nothing else is known. */
|
|
37
|
+
defaultLocale: Locale;
|
|
38
|
+
/** Query key; defaults to `["page-context"]`. */
|
|
39
|
+
queryKey?: readonly unknown[];
|
|
40
|
+
/** Cache lifetime in ms; defaults to one minute. */
|
|
41
|
+
staleTime?: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type SitePageContext<Row> = {
|
|
45
|
+
pageContextQueryOptions: ReturnType<typeof queryOptions<PageContext<Row>>>;
|
|
46
|
+
buildPageHead: (pageKey: string, context: PageContext<Row> | undefined) => PageHead;
|
|
47
|
+
canonicalPathFor: (pageKey: string, context: PageContext<Row> | undefined) => string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Build the page-context loader + head helpers for one site. */
|
|
51
|
+
export function createPageContext<Row>(deps: PageContextDeps<Row>): SitePageContext<Row> {
|
|
52
|
+
async function fetchPageContext(): Promise<PageContext<Row>> {
|
|
53
|
+
const [seoRows, localeInfo, domains] = await Promise.all([
|
|
54
|
+
deps.getSeoRows(),
|
|
55
|
+
deps.getRequestLocale(),
|
|
56
|
+
deps.getLocaleDomains(),
|
|
57
|
+
]);
|
|
58
|
+
return {
|
|
59
|
+
seoRows,
|
|
60
|
+
locale: localeInfo.locale,
|
|
61
|
+
origin: localeInfo.origin,
|
|
62
|
+
domains,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const options = queryOptions<PageContext<Row>>({
|
|
67
|
+
queryKey: deps.queryKey ?? ["page-context"],
|
|
68
|
+
queryFn: fetchPageContext,
|
|
69
|
+
staleTime: deps.staleTime ?? 60_000,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
function buildPageHead(pageKey: string, context: PageContext<Row> | undefined): PageHead {
|
|
73
|
+
const locale = context?.locale ?? deps.defaultLocale;
|
|
74
|
+
const origin = context?.origin ?? "";
|
|
75
|
+
const overrides = deps.rowsToOverrides(context?.seoRows);
|
|
76
|
+
const domains = context?.domains ?? [];
|
|
77
|
+
return deps.routes.pageHeadMeta(pageKey, locale, origin, overrides, domains);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function canonicalPathFor(pageKey: string, context: PageContext<Row> | undefined): string {
|
|
81
|
+
const locale = context?.locale ?? deps.defaultLocale;
|
|
82
|
+
const overrides = deps.rowsToOverrides(context?.seoRows);
|
|
83
|
+
return deps.routes.resolveRoute(pageKey, locale, overrides);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { pageContextQueryOptions: options, buildPageHead, canonicalPathFor };
|
|
87
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canonical public origin for a deployment.
|
|
3
|
+
*
|
|
4
|
+
* The origin is a deployment fact, not editable content: preview, staging and
|
|
5
|
+
* production are different hosts. It is read from the environment, with the
|
|
6
|
+
* origin compiled into the site's source as the fallback, and is deliberately
|
|
7
|
+
* never stored as a database override — canonical URLs, sitemaps and auth
|
|
8
|
+
* redirects must be right before any database read happens.
|
|
9
|
+
*/
|
|
10
|
+
import { originIsFromEnvironment, resolveSiteOrigin } from "../config/site-config";
|
|
11
|
+
|
|
12
|
+
export type PublicOriginDeps = {
|
|
13
|
+
/** Origin compiled into the build; used whenever no environment value is set. */
|
|
14
|
+
codeOrigin: string;
|
|
15
|
+
/** Reads the configured origin from the environment, or `undefined`. */
|
|
16
|
+
readEnvOrigin: () => string | undefined;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type PublicOrigin = {
|
|
20
|
+
/** The origin this deployment should advertise. Always returns something. */
|
|
21
|
+
publicOrigin: () => string;
|
|
22
|
+
/**
|
|
23
|
+
* The origin this deployment was EXPLICITLY configured with, or `null`.
|
|
24
|
+
*
|
|
25
|
+
* Distinct from `publicOrigin()`: a deployment with no configuration is not
|
|
26
|
+
* the live site, it is a deployment whose configuration is missing. Answering
|
|
27
|
+
* that from a constant would make every unconfigured build claim to be
|
|
28
|
+
* production — the one answer that must never be reached by default.
|
|
29
|
+
*/
|
|
30
|
+
configuredPublicOrigin: () => string | null;
|
|
31
|
+
/** Whether the running origin came from the environment rather than code. */
|
|
32
|
+
publicOriginFromEnv: () => boolean;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Build the origin accessors for one deployment. */
|
|
36
|
+
export function createPublicOrigin(deps: PublicOriginDeps): PublicOrigin {
|
|
37
|
+
return {
|
|
38
|
+
publicOrigin: () => resolveSiteOrigin(deps.codeOrigin, deps.readEnvOrigin()),
|
|
39
|
+
configuredPublicOrigin: () => {
|
|
40
|
+
const value = deps.readEnvOrigin();
|
|
41
|
+
if (typeof value !== "string") return null;
|
|
42
|
+
const trimmed = value.trim();
|
|
43
|
+
return trimmed ? trimmed : null;
|
|
44
|
+
},
|
|
45
|
+
publicOriginFromEnv: () => originIsFromEnvironment(deps.codeOrigin, deps.readEnvOrigin()),
|
|
46
|
+
};
|
|
47
|
+
}
|