@suiteop/storefront 0.4.0 → 0.6.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 +56 -0
- package/README.md +9 -2
- package/dist/astro-content-model.cjs +5 -5
- package/dist/astro-content-model.js +4 -4
- package/dist/astro.cjs +1 -1
- package/dist/astro.d.cts +66 -1
- package/dist/astro.d.ts +66 -1
- package/dist/astro.js +1 -1
- package/dist/availability.d-Cla9GuGc.d.cts +84 -0
- package/dist/availability.d-Cla9GuGc.d.ts +84 -0
- package/dist/checkout.cjs +4 -4
- package/dist/checkout.d.cts +11 -0
- package/dist/checkout.d.ts +11 -0
- package/dist/checkout.js +3 -3
- package/dist/{chunk-TLTL7M3O.js → chunk-3B6ZYRYF.js} +2 -2
- package/dist/chunk-3NE27PHL.cjs +19 -0
- package/dist/{chunk-HMLBMNGA.js → chunk-7ZAI36LH.js} +1 -1
- package/dist/{chunk-LVFRVNT3.cjs → chunk-EHQFWSP6.cjs} +2 -1
- package/dist/{chunk-6NFURJXI.js → chunk-IBCBDAGY.js} +2 -2
- package/dist/{chunk-USOISC2R.cjs → chunk-J5FZ4WDQ.cjs} +2 -2
- package/dist/chunk-JF245QUY.cjs +9 -0
- package/dist/{chunk-IUJTHNB3.cjs → chunk-OXCLLFCY.cjs} +3 -3
- package/dist/chunk-QBHOJGZI.cjs +21 -0
- package/dist/{chunk-VV2UNVQ2.cjs → chunk-UMCI4YBN.cjs} +2 -2
- package/dist/chunk-UQWQPTHO.js +3 -0
- package/dist/{chunk-ZI2PG7LS.js → chunk-W5K7IDYN.js} +1 -1
- package/dist/chunk-XOCFEVP4.js +6 -0
- package/dist/chunk-YIYGBTAM.js +9 -0
- package/dist/{client-oyYRhUv0.d.ts → client-CX8K7JNt.d.cts} +108 -20
- package/dist/{client-CZWN4im4.d.cts → client-s-lEwqzc.d.ts} +108 -20
- package/dist/components.cjs +2 -2
- package/dist/components.d.cts +43 -1
- package/dist/components.d.ts +43 -1
- package/dist/components.js +1 -1
- package/dist/config.cjs +10 -3
- package/dist/config.d.cts +240 -79
- package/dist/config.d.ts +240 -79
- package/dist/config.js +4 -4
- package/dist/content.cjs +11 -11
- package/dist/content.d.cts +53 -9
- package/dist/content.d.ts +53 -9
- package/dist/content.js +4 -4
- package/dist/{errors-CAW7nLHr.d.cts → errors-Oax7djdp.d.ts} +54 -13
- package/dist/{errors-DoXdi1vS.d.ts → errors-welPvrD6.d.cts} +54 -13
- package/dist/index.cjs +22 -22
- package/dist/index.d.cts +27 -8
- package/dist/index.d.ts +27 -8
- package/dist/index.js +5 -5
- package/dist/react.cjs +7 -7
- package/dist/react.d.cts +24 -3
- package/dist/react.d.ts +24 -3
- package/dist/react.js +6 -6
- package/package.json +1 -1
- package/dist/availability-CnzmxuV3.d.cts +0 -60
- package/dist/availability-CnzmxuV3.d.ts +0 -60
- package/dist/chunk-CTHHMNLE.cjs +0 -19
- package/dist/chunk-E5CGGMCO.cjs +0 -19
- package/dist/chunk-PYVUDQSQ.js +0 -3
- package/dist/chunk-TRXDDUEL.js +0 -5
- package/dist/chunk-UWUKERLK.js +0 -9
- package/dist/chunk-ZLAOY6IF.cjs +0 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,62 @@
|
|
|
2
2
|
|
|
3
3
|
Hand-written per release. Tag `storefront-sdk@<version>` on `main` publishes it.
|
|
4
4
|
|
|
5
|
+
## 0.6.0
|
|
6
|
+
|
|
7
|
+
- `suiteop({ routes })` takes two more sitemap sources: `guides` (one URL per guide the site shows,
|
|
8
|
+
e.g. `'/things-to-do/[id]': 'guides'`) and `page`, a server-rendered route without a parameter
|
|
9
|
+
listed as one page (`'/': 'page'`, `'/blog': 'page'`). A `page` route with a parameter is
|
|
10
|
+
refused at config time. An older platform skips both sources; the rest of the sitemap is
|
|
11
|
+
unchanged.
|
|
12
|
+
- `listings.list({ checkIn, checkOut })`: only listings whose synced calendar can take that stay
|
|
13
|
+
(listing-local `YYYY-MM-DD`, checkout exclusive; both or neither, with the stay rules of
|
|
14
|
+
`availability.get`) — every night available, the check-in day open to arrival and its minimum
|
|
15
|
+
stay met, the checkout day open to departure. `total`, paging and `facets` count only those
|
|
16
|
+
listings. Display only, as `buildings.availability`: `quotes.create` re-checks live. An older API
|
|
17
|
+
ignores both and answers every listing.
|
|
18
|
+
|
|
19
|
+
## 0.5.0
|
|
20
|
+
|
|
21
|
+
- The site config takes optional `layout` (`listings`, the default, `locations`, `areas` or
|
|
22
|
+
`single`) and `theme` (`tide`, the default, `folio`, `hearth`, `circuit`, `noir` or `fjord`).
|
|
23
|
+
A value this version does not know reads as absent, so a site never fails its build on a newer
|
|
24
|
+
one.
|
|
25
|
+
- `colors.primary` is the only colour a config needs: `colors.secondary` (optional) is a second
|
|
26
|
+
accent, and `background`, `foreground`, `accent` and `primaryForeground` become optional
|
|
27
|
+
overrides of the colours the theme derives. `brand.logoOnDarkUrl` (optional) is the logo for
|
|
28
|
+
dark grounds and photo heroes. `fonts` and each of its two fields are optional: absent takes the
|
|
29
|
+
theme's pair. Every config valid for 0.4 stays valid.
|
|
30
|
+
- `@suiteop/storefront/config` exports `resolveStorefrontTheme(config)` and
|
|
31
|
+
`deriveStorefrontTheme(theme, brandHex, options)`: the whole `--so-*` token set (surfaces,
|
|
32
|
+
tints, borders, text, buttons, dark sections, focus, scrims, shadows), the `--t-*` type scale,
|
|
33
|
+
radii and button metrics, the WCAG checks, notes on what the brand colour needed, and the
|
|
34
|
+
overrides refused for failing AA. `STOREFRONT_THEME_RECIPES` holds each theme's recipe.
|
|
35
|
+
- `buildings.availability(id, { checkIn, checkOut, guests })`: every unit of a building for one
|
|
36
|
+
stay in a single call — `available`, `reason`, the stay's `totalMinor` and `currency`, `nights`,
|
|
37
|
+
`maxGuests`, and `nextAvailable`, the first free stay of the same length starting within 60 days
|
|
38
|
+
after the requested check-in. Display only: it reads the synced calendar, and the total is the
|
|
39
|
+
nightly rates plus the cleaning fee, before taxes — `quotes.create` gives the price a guest
|
|
40
|
+
books at. Pages like `listings.list` (`limit` up to 100, default 50). Types
|
|
41
|
+
`StorefrontUnitAvailability`, `StorefrontUnitUnavailableReason` and
|
|
42
|
+
`StorefrontBuildingAvailabilityQuery`.
|
|
43
|
+
- Listing summaries carry `fromPriceMinor` and `currency`, computed as the listing detail computes
|
|
44
|
+
them, so a search page no longer needs one detail request per card. Both are nullish: an older
|
|
45
|
+
API omits them.
|
|
46
|
+
- `listings.list()` takes search filters, all optional: `guests` (sleeps at least), `bedrooms` (at
|
|
47
|
+
least), `currency` (ISO 4217: only listings priced in it), `priceMin` / `priceMax` (on
|
|
48
|
+
`fromPriceMinor`, minor units of `currency`, which they require; an unpriced listing is left
|
|
49
|
+
out), `amenities` (all of them, as `amenities.list()` names them), `bounds`
|
|
50
|
+
(`[swLat, swLng, neLat, neLng]`, wrapping the antimeridian; a listing without coordinates is
|
|
51
|
+
outside), and `location` now takes several areas (any of them). `sort`: `recommended` (the
|
|
52
|
+
manager's order, the default), `price_asc`, `price_desc` (by amount within each currency),
|
|
53
|
+
`rating`; unpriced or unrated listings sort last.
|
|
54
|
+
- `listings.list({ facets: true })` also answers `facets` for a filters sheet: `total`, per-area
|
|
55
|
+
and per-bedroom counts, and a 24-bucket `fromPriceMinor` histogram with its min, max and
|
|
56
|
+
currency (null when the matches price in several currencies and no `currency` picks one). Each
|
|
57
|
+
facet ignores its own filter. Type `StorefrontListingFacets`.
|
|
58
|
+
- `StorefrontListingsQuery` is now the call's own shape (every key optional, `location` a string
|
|
59
|
+
or a list); `listings.list` takes it directly instead of `Partial<StorefrontListingsQuery>`.
|
|
60
|
+
|
|
5
61
|
## 0.4.0
|
|
6
62
|
|
|
7
63
|
- `guides.list()` / `guides.get(id)`: the "Things to do" lists the manager shows on the site
|
package/README.md
CHANGED
|
@@ -225,10 +225,17 @@ import { suiteop } from '@suiteop/storefront/astro'
|
|
|
225
225
|
export default defineConfig({ integrations: [suiteop()] })
|
|
226
226
|
```
|
|
227
227
|
|
|
228
|
+
`layout` and `theme` in the config are optional (`listings` and `tide` when absent).
|
|
229
|
+
`resolveStorefrontTheme(config)` (`@suiteop/storefront/config`) derives the theme's whole token
|
|
230
|
+
set from `colors.primary` — the `--so-*` colours, type scale, radii and button metrics to set on
|
|
231
|
+
`:root` — with WCAG AA checked for every text pair.
|
|
232
|
+
|
|
228
233
|
`suiteop()` writes `/_suiteop/manifest.json` after every build: the prerendered pages (path,
|
|
229
234
|
language, title, last change — pages marked `noindex` are left out) and the on-demand routes that
|
|
230
|
-
expand from catalogue data (`/listing/[id]`, `/building/[id]`).
|
|
231
|
-
|
|
235
|
+
expand from catalogue data (`/listing/[id]`, `/building/[id]`). Declare the others in
|
|
236
|
+
`suiteop({ routes })`: `'/things-to-do/[id]': 'guides'` for the guides the site shows, and
|
|
237
|
+
`'page'` for a server-rendered page without a parameter (`'/': 'page'`, `'/blog': 'page'`), which
|
|
238
|
+
the sitemap then lists once. `/_suiteop/*` is reserved for the platform; do not add pages under it.
|
|
232
239
|
|
|
233
240
|
On SuiteOp hosting, `getSiteContext(Astro.locals)` (`@suiteop/storefront/astro`) returns the
|
|
234
241
|
per-request site props the edge passes (publishable key, API bases; null elsewhere). The checkout
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
require('./chunk-
|
|
5
|
-
require('./chunk-
|
|
6
|
-
require('./chunk-
|
|
3
|
+
var chunkOXCLLFCY_cjs = require('./chunk-OXCLLFCY.cjs');
|
|
4
|
+
require('./chunk-QBHOJGZI.cjs');
|
|
5
|
+
require('./chunk-J5FZ4WDQ.cjs');
|
|
6
|
+
require('./chunk-EHQFWSP6.cjs');
|
|
7
7
|
var definition = require('virtual:suiteop/content');
|
|
8
8
|
|
|
9
9
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
10
|
|
|
11
11
|
var definition__default = /*#__PURE__*/_interopDefault(definition);
|
|
12
12
|
|
|
13
|
-
var prerender=true;function GET(){return new Response(`${JSON.stringify(
|
|
13
|
+
var prerender=true;function GET(){return new Response(`${JSON.stringify(chunkOXCLLFCY_cjs.parseContentModel(definition__default.default))}
|
|
14
14
|
`,{headers:{"content-type":"application/json"}})}
|
|
15
15
|
|
|
16
16
|
exports.GET = GET;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { parseContentModel } from './chunk-
|
|
2
|
-
import './chunk-
|
|
3
|
-
import './chunk-
|
|
4
|
-
import './chunk-
|
|
1
|
+
import { parseContentModel } from './chunk-3B6ZYRYF.js';
|
|
2
|
+
import './chunk-XOCFEVP4.js';
|
|
3
|
+
import './chunk-W5K7IDYN.js';
|
|
4
|
+
import './chunk-IBCBDAGY.js';
|
|
5
5
|
import definition from 'virtual:suiteop/content';
|
|
6
6
|
|
|
7
7
|
var prerender=true;function GET(){return new Response(`${JSON.stringify(parseContentModel(definition))}
|
package/dist/astro.cjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var zod = require('zod');
|
|
4
4
|
|
|
5
|
-
var SOURCES=[[/^\/listing\/\[[^\]/]+\]$/,"listings"],[/^\/building\/\[[^\]/]+\]$/,"buildings"],[/^\/area\/\[[^\]/]+\]$/,"locations"]];var DECLARABLE_SOURCE=/^(listings|buildings|locations|collection:[a-z][a-z0-9_]{0,39})$/;var ERROR_PAGES=new Set(["404","500"]);function normalizePath(pathname){const withSlash=pathname.startsWith("/")?pathname:`/${pathname}`;const trimmed=withSlash.replace(/\/+$/,"").replace(/\/index(\.html)?$/,"");return trimmed===""?"/":trimmed}var ENTITIES={amp:"&",lt:"<",gt:">",quot:'"',apos:"'"};function decodeEntities(text){return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi,(whole,ref)=>{if(ref[0]==="#"){const code=ref[1]==="x"||ref[1]==="X"?parseInt(ref.slice(2),16):Number(ref.slice(1));return Number.isFinite(code)&&code>0&&code<=1114111?String.fromCodePoint(code):whole}return ENTITIES[ref.toLowerCase()]??whole})}function metaContent(html,name){for(const[tag]of html.matchAll(/<meta\b[^>]*>/gi)){const attr=key=>new RegExp(`\\b${key}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`,"i").exec(tag);const tagName=attr("name");if((tagName?.[1]??tagName?.[2])?.toLowerCase()!==name)continue;const content=attr("content");return decodeEntities(content?.[1]??content?.[2]??"")}return null}function localeOf(path,config){const first=path.split("/")[1]??"";return first!==config.defaultLocale&&config.locales.includes(first)?first:config.defaultLocale}function isErrorPage(path,locale,config){const rest=locale===config.defaultLocale?path:path.slice(locale.length+1);return ERROR_PAGES.has(rest.replace(/^\//,""))}async function buildSiteManifest(input){const now=input.now.toISOString();const pages=[];const seen=new Set;for(const pathname of input.pages){const path=normalizePath(pathname);if(seen.has(path))continue;seen.add(path);const locale=localeOf(path,input.config);if(isErrorPage(path,locale,input.config))continue;const html=await input.readHtml(pathname);if(!html)continue;if(/\bnoindex\b/i.test(metaContent(html,"robots")??""))continue;const title=/<title[^>]*>([\s\S]*?)<\/title>/i.exec(html)?.[1];const lastmodRaw=metaContent(html,"suiteop:lastmod");const lastmodMs=lastmodRaw?Date.parse(lastmodRaw):Number.NaN;pages.push({path,locale,title:title?decodeEntities(title.trim()):"",lastmod:Number.isNaN(lastmodMs)?now:new Date(lastmodMs).toISOString()});}pages.sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0);const dynamicRoutes=[];for(const pattern of input.dynamicRoutes){const source=input.declaredRoutes?.[pattern]??SOURCES.find(([re])=>re.test(pattern))?.[1];if(source&&!dynamicRoutes.some(r=>r.pattern===pattern)){dynamicRoutes.push({pattern,source});}}return {version:1,generatedAt:now,pages,dynamicRoutes}}var sameOriginPath=zod.z.string().regex(/^\/(?![/\\])/);var siteContextSchema=zod.z.object({siteId:zod.z.string().min(1),orgId:zod.z.string().min(1),region:zod.z.enum(["us","eu","apac"]),environment:zod.z.enum(["live","test"]),publishableKey:zod.z.string().regex(/^pk_(live|test)_/),country:zod.z.string().nullable().optional(),apiBasePath:sameOriginPath,apiBaseUrl:zod.z.url({protocol:/^https$/}).nullable().optional(),apiHeaders:zod.z.record(zod.z.string().regex(/^x-suiteop-[a-z0-9-]+$/),zod.z.string()).optional()});function getSiteContext(locals){const props=locals.cfContext?.props;if(typeof props!=="object"||props===null)return null;const parsed=siteContextSchema.safeParse(props);if(!parsed.success)return null;const p=parsed.data;return {...p,country:p.country??null,apiBaseUrl:p.apiBaseUrl??null,apiHeaders:p.apiHeaders??{}}}var SITE_MANIFEST_PATH="_suiteop/manifest.json";var CONTENT_MODEL_PATH="_suiteop/content-model.json";var CONTENT_MODULE_ID="virtual:suiteop/content";var RESOLVED_CONTENT_MODULE_ID=`\0${CONTENT_MODULE_ID}`;function devProxy(env){const apiOrigin=env["SUITEOP_API_ORIGIN"];const edgeOrigin=env["SUITEOP_EDGE_ORIGIN"];const proxy={};if(apiOrigin){proxy["/api/storefront/v1"]={target:apiOrigin,changeOrigin:true,...edgeOrigin?{headers:{origin:edgeOrigin}}:{}};}if(edgeOrigin){proxy["^/checkout/(manifest\\.json|v\\d+(\\.[0-9a-f]+)?\\.js)$"]={target:edgeOrigin,changeOrigin:true};}return proxy}function contentModulePlugin(file){return {name:"@suiteop/storefront:content",resolveId:id=>id===CONTENT_MODULE_ID?RESOLVED_CONTENT_MODULE_ID:void 0,load:id=>id===RESOLVED_CONTENT_MODULE_ID?`export { default } from ${JSON.stringify(file)}`:void 0}}function suiteop(options={}){for(const[pattern,source]of Object.entries(options.routes??{})){if(!DECLARABLE_SOURCE.test(source)){throw new Error(`suiteop(): route ${pattern} has source "${source}"; use listings, buildings, locations or collection:<key>.`)}}let i18n={locales:[],defaultLocale:"en"};let format="directory";let onDemandRoutes=[];return {name:"@suiteop/storefront",hooks:{"astro:config:setup":({command,config,updateConfig,injectRoute})=>{if(options.content){const file=decodeURIComponent(new URL(options.content,config.root).pathname);updateConfig({vite:{plugins:[contentModulePlugin(file)]}});injectRoute({pattern:`/${CONTENT_MODEL_PATH}`,entrypoint:"@suiteop/storefront/astro/content-model",prerender:true});}if(command==="dev"&&process.env["SUITEOP_DEV_PROXY"]==="1"){updateConfig({vite:{server:{proxy:devProxy(process.env),strictPort:true}}});}},"astro:config:done":({config})=>{format=config.build.format;if(config.i18n){i18n={locales:config.i18n.locales.map(l=>typeof l==="string"?l:l.path),defaultLocale:config.i18n.defaultLocale};}},"astro:routes:resolved":({routes})=>{onDemandRoutes=routes.filter(r=>r.type==="page"&&!r.isPrerendered&&r.origin==="project").map(r=>r.pattern);},"astro:build:done":async({dir,pages,logger})=>{const{mkdir,writeFile}=await import('node:fs/promises');const built=await builtHtmlPaths(dir);const all=new Set([...pages.map(p=>`/${p.pathname}`),...built]);const manifest=await buildSiteManifest({pages:[...all],dynamicRoutes:onDemandRoutes,declaredRoutes:options.routes,readHtml:pathname=>readBuiltHtml(dir,pathname,format),now:new Date,config:i18n});const target=new URL(SITE_MANIFEST_PATH,dir);await mkdir(new URL(".",target),{recursive:true});await writeFile(target,`${JSON.stringify(manifest)}
|
|
5
|
+
var SOURCES=[[/^\/listing\/\[[^\]/]+\]$/,"listings"],[/^\/building\/\[[^\]/]+\]$/,"buildings"],[/^\/area\/\[[^\]/]+\]$/,"locations"]];var DECLARABLE_SOURCE=/^(listings|buildings|locations|guides|page|collection:[a-z][a-z0-9_]{0,39})$/;var ERROR_PAGES=new Set(["404","500"]);function normalizePath(pathname){const withSlash=pathname.startsWith("/")?pathname:`/${pathname}`;const trimmed=withSlash.replace(/\/+$/,"").replace(/\/index(\.html)?$/,"");return trimmed===""?"/":trimmed}var ENTITIES={amp:"&",lt:"<",gt:">",quot:'"',apos:"'"};function decodeEntities(text){return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi,(whole,ref)=>{if(ref[0]==="#"){const code=ref[1]==="x"||ref[1]==="X"?parseInt(ref.slice(2),16):Number(ref.slice(1));return Number.isFinite(code)&&code>0&&code<=1114111?String.fromCodePoint(code):whole}return ENTITIES[ref.toLowerCase()]??whole})}function metaContent(html,name){for(const[tag]of html.matchAll(/<meta\b[^>]*>/gi)){const attr=key=>new RegExp(`\\b${key}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`,"i").exec(tag);const tagName=attr("name");if((tagName?.[1]??tagName?.[2])?.toLowerCase()!==name)continue;const content=attr("content");return decodeEntities(content?.[1]??content?.[2]??"")}return null}function localeOf(path,config){const first=path.split("/")[1]??"";return first!==config.defaultLocale&&config.locales.includes(first)?first:config.defaultLocale}function isErrorPage(path,locale,config){const rest=locale===config.defaultLocale?path:path.slice(locale.length+1);return ERROR_PAGES.has(rest.replace(/^\//,""))}async function buildSiteManifest(input){const now=input.now.toISOString();const pages=[];const seen=new Set;for(const pathname of input.pages){const path=normalizePath(pathname);if(seen.has(path))continue;seen.add(path);const locale=localeOf(path,input.config);if(isErrorPage(path,locale,input.config))continue;const html=await input.readHtml(pathname);if(!html)continue;if(/\bnoindex\b/i.test(metaContent(html,"robots")??""))continue;const title=/<title[^>]*>([\s\S]*?)<\/title>/i.exec(html)?.[1];const lastmodRaw=metaContent(html,"suiteop:lastmod");const lastmodMs=lastmodRaw?Date.parse(lastmodRaw):Number.NaN;pages.push({path,locale,title:title?decodeEntities(title.trim()):"",lastmod:Number.isNaN(lastmodMs)?now:new Date(lastmodMs).toISOString()});}pages.sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0);const dynamicRoutes=[];for(const pattern of input.dynamicRoutes){const source=input.declaredRoutes?.[pattern]??SOURCES.find(([re])=>re.test(pattern))?.[1];if(source&&!dynamicRoutes.some(r=>r.pattern===pattern)){dynamicRoutes.push({pattern,source});}}return {version:1,generatedAt:now,pages,dynamicRoutes}}var sameOriginPath=zod.z.string().regex(/^\/(?![/\\])/);var siteContextSchema=zod.z.object({siteId:zod.z.string().min(1),orgId:zod.z.string().min(1),region:zod.z.enum(["us","eu","apac"]),environment:zod.z.enum(["live","test"]),publishableKey:zod.z.string().regex(/^pk_(live|test)_/),country:zod.z.string().nullable().optional(),apiBasePath:sameOriginPath,apiBaseUrl:zod.z.url({protocol:/^https$/}).nullable().optional(),apiHeaders:zod.z.record(zod.z.string().regex(/^x-suiteop-[a-z0-9-]+$/),zod.z.string()).optional()});function getSiteContext(locals){const props=locals.cfContext?.props;if(typeof props!=="object"||props===null)return null;const parsed=siteContextSchema.safeParse(props);if(!parsed.success)return null;const p=parsed.data;return {...p,country:p.country??null,apiBaseUrl:p.apiBaseUrl??null,apiHeaders:p.apiHeaders??{}}}var SITE_MANIFEST_PATH="_suiteop/manifest.json";var CONTENT_MODEL_PATH="_suiteop/content-model.json";var CONTENT_MODULE_ID="virtual:suiteop/content";var RESOLVED_CONTENT_MODULE_ID=`\0${CONTENT_MODULE_ID}`;function devProxy(env){const apiOrigin=env["SUITEOP_API_ORIGIN"];const edgeOrigin=env["SUITEOP_EDGE_ORIGIN"];const proxy={};if(apiOrigin){proxy["/api/storefront/v1"]={target:apiOrigin,changeOrigin:true,...edgeOrigin?{headers:{origin:edgeOrigin}}:{}};}if(edgeOrigin){proxy["^/checkout/(manifest\\.json|v\\d+(\\.[0-9a-f]+)?\\.js)$"]={target:edgeOrigin,changeOrigin:true};}return proxy}function contentModulePlugin(file){return {name:"@suiteop/storefront:content",resolveId:id=>id===CONTENT_MODULE_ID?RESOLVED_CONTENT_MODULE_ID:void 0,load:id=>id===RESOLVED_CONTENT_MODULE_ID?`export { default } from ${JSON.stringify(file)}`:void 0}}function suiteop(options={}){for(const[pattern,source]of Object.entries(options.routes??{})){if(!DECLARABLE_SOURCE.test(source)){throw new Error(`suiteop(): route ${pattern} has source "${source}"; use listings, buildings, locations, guides, page or collection:<key>.`)}if(source==="page"&&pattern.includes("[")){throw new Error(`suiteop(): route ${pattern} has a parameter, so it is not one page.`)}}let i18n={locales:[],defaultLocale:"en"};let format="directory";let onDemandRoutes=[];return {name:"@suiteop/storefront",hooks:{"astro:config:setup":({command,config,updateConfig,injectRoute})=>{if(options.content){const file=decodeURIComponent(new URL(options.content,config.root).pathname);updateConfig({vite:{plugins:[contentModulePlugin(file)]}});injectRoute({pattern:`/${CONTENT_MODEL_PATH}`,entrypoint:"@suiteop/storefront/astro/content-model",prerender:true});}if(command==="dev"&&process.env["SUITEOP_DEV_PROXY"]==="1"){updateConfig({vite:{server:{proxy:devProxy(process.env),strictPort:true}}});}},"astro:config:done":({config})=>{format=config.build.format;if(config.i18n){i18n={locales:config.i18n.locales.map(l=>typeof l==="string"?l:l.path),defaultLocale:config.i18n.defaultLocale};}},"astro:routes:resolved":({routes})=>{onDemandRoutes=routes.filter(r=>r.type==="page"&&!r.isPrerendered&&r.origin==="project").map(r=>r.pattern);},"astro:build:done":async({dir,pages,logger})=>{const{mkdir,writeFile}=await import('node:fs/promises');const built=await builtHtmlPaths(dir);const all=new Set([...pages.map(p=>`/${p.pathname}`),...built]);const manifest=await buildSiteManifest({pages:[...all],dynamicRoutes:onDemandRoutes,declaredRoutes:options.routes,readHtml:pathname=>readBuiltHtml(dir,pathname,format),now:new Date,config:i18n});const target=new URL(SITE_MANIFEST_PATH,dir);await mkdir(new URL(".",target),{recursive:true});await writeFile(target,`${JSON.stringify(manifest)}
|
|
6
6
|
`);logger.info(`suiteop manifest: ${manifest.pages.length} pages, ${manifest.dynamicRoutes.length} dynamic routes`);}}}}async function builtHtmlPaths(dir){const{readdir}=await import('node:fs/promises');const{fileURLToPath}=await import('node:url');const entries=await readdir(fileURLToPath(dir),{recursive:true,withFileTypes:true});const paths=[];for(const entry of entries){if(!entry.isFile()||!entry.name.endsWith(".html"))continue;const rel=`${entry.parentPath}/${entry.name}`.slice(fileURLToPath(dir).length).replace(/\\/g,"/").replace(/^\/+/,"");if(/^(_astro|_suiteop)\//.test(rel))continue;paths.push(`/${rel.replace(/(^|\/)index\.html$/,"$1").replace(/\.html$/,"")}`);}return paths}async function readBuiltHtml(dir,pathname,format){const{readFile}=await import('node:fs/promises');const base=pathname.replace(/^\/+/,"").replace(/\/+$/,"");const candidates=base===""?["index.html"]:format==="file"?[`${base}.html`,`${base}/index.html`]:[`${base}/index.html`,`${base}.html`];for(const candidate of candidates){try{return await readFile(new URL(candidate,dir),"utf8")}catch(err){if(err.code!=="ENOENT")throw err}}return null}
|
|
7
7
|
|
|
8
8
|
exports.CONTENT_MODEL_PATH = CONTENT_MODEL_PATH;
|
package/dist/astro.d.cts
CHANGED
|
@@ -1,23 +1,45 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The site manifest a storefront build emits at `/_suiteop/manifest.json`. The platform reads it
|
|
3
|
+
* after a deploy for the sitemap and search features: which prerendered pages exist (with their
|
|
4
|
+
* title, language and last change) and which on-demand routes expand from catalogue data.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Where a dynamic route's pages come from: the listings, buildings or areas (`locations`) of the
|
|
8
|
+
* catalogue, the guides the site shows (`guides`), or the published items of one of the site's
|
|
9
|
+
* collections (`collection:<key>`). `page` is a server-rendered route without a parameter
|
|
10
|
+
* (`/blog`): the sitemap lists it as one page.
|
|
11
|
+
*/
|
|
12
|
+
type SiteManifestSource = 'listings' | 'buildings' | 'locations' | 'guides' | 'page' | `collection:${string}`;
|
|
2
13
|
interface SiteManifestPage {
|
|
14
|
+
/** Site-relative, no trailing slash (`/` for the home page). */
|
|
3
15
|
path: string;
|
|
4
16
|
locale: string;
|
|
5
17
|
title: string;
|
|
18
|
+
/** ISO 8601. */
|
|
6
19
|
lastmod: string;
|
|
7
20
|
}
|
|
8
21
|
interface SiteManifest {
|
|
9
22
|
version: 1;
|
|
10
23
|
generatedAt: string;
|
|
11
24
|
pages: SiteManifestPage[];
|
|
25
|
+
/** On-demand routes whose pages are one per catalogue record. Checkout, confirmation and search never appear. */
|
|
12
26
|
dynamicRoutes: {
|
|
13
27
|
pattern: string;
|
|
14
28
|
source: SiteManifestSource;
|
|
15
29
|
}[];
|
|
16
30
|
}
|
|
17
31
|
interface BuildSiteManifestInput {
|
|
32
|
+
/** Pathnames of the prerendered pages, as the build reports them. */
|
|
18
33
|
pages: string[];
|
|
34
|
+
/** Route patterns of the on-demand (server-rendered) routes, e.g. `/listing/[id]`. */
|
|
19
35
|
dynamicRoutes: string[];
|
|
36
|
+
/**
|
|
37
|
+
* Sources the site declares for routes the conventions do not cover
|
|
38
|
+
* (`suiteop({ routes: { '/blog/[slug]': 'collection:blog' } })`). Only on-demand routes count:
|
|
39
|
+
* a prerendered page is listed as a page already.
|
|
40
|
+
*/
|
|
20
41
|
declaredRoutes?: Record<string, SiteManifestSource>;
|
|
42
|
+
/** The built HTML for a page pathname; null when it cannot be read. */
|
|
21
43
|
readHtml(pathname: string): Promise<string | null>;
|
|
22
44
|
now: Date;
|
|
23
45
|
config: {
|
|
@@ -27,30 +49,47 @@ interface BuildSiteManifestInput {
|
|
|
27
49
|
}
|
|
28
50
|
declare function buildSiteManifest(input: BuildSiteManifestInput): Promise<SiteManifest>;
|
|
29
51
|
|
|
52
|
+
/**
|
|
53
|
+
* What the SuiteOp edge hands every request of a SuiteOp-hosted site on the Worker's
|
|
54
|
+
* `ctx.props` (`Astro.locals.cfContext.props` with `@astrojs/cloudflare`).
|
|
55
|
+
*/
|
|
30
56
|
interface SiteContext {
|
|
31
57
|
siteId: string;
|
|
32
58
|
orgId: string;
|
|
33
59
|
region: 'us' | 'eu' | 'apac';
|
|
34
60
|
environment: 'live' | 'test';
|
|
61
|
+
/** This environment's publishable key. Public by design — safe in HTML. */
|
|
35
62
|
publishableKey: string;
|
|
63
|
+
/** The visitor's country (ISO 3166-1 alpha-2) when the edge knows it. */
|
|
36
64
|
country: string | null;
|
|
65
|
+
/** Same-origin Storefront API path for the browser, proxied by the edge. */
|
|
37
66
|
apiBasePath: string;
|
|
67
|
+
/**
|
|
68
|
+
* Absolute Storefront API base for server-side calls (the site's regional API). Null when the
|
|
69
|
+
* edge does not provide one; server calls then fall back to the same-origin path.
|
|
70
|
+
*/
|
|
38
71
|
apiBaseUrl: string | null;
|
|
72
|
+
/** Headers the edge signs for THIS request (the visitor's IP); send them on server-side API calls. */
|
|
39
73
|
apiHeaders: Record<string, string>;
|
|
40
74
|
}
|
|
75
|
+
/** The edge's props for this request, or null when the site is not served by the SuiteOp edge. */
|
|
41
76
|
declare function getSiteContext(locals: object): SiteContext | null;
|
|
42
77
|
|
|
78
|
+
/** Where the manifest lands in the client build output, and the path the site serves it on. */
|
|
43
79
|
declare const SITE_MANIFEST_PATH = "_suiteop/manifest.json";
|
|
80
|
+
/** Where the site's content model lands in the client build output (`suiteop({ content })`). */
|
|
44
81
|
declare const CONTENT_MODEL_PATH = "_suiteop/content-model.json";
|
|
45
82
|
interface ProxyTarget {
|
|
46
83
|
target: string;
|
|
47
84
|
changeOrigin: boolean;
|
|
85
|
+
/** Set on every proxied request, replacing what the caller sent. */
|
|
48
86
|
headers?: Record<string, string>;
|
|
49
87
|
rewrite?: (path: string) => string;
|
|
50
88
|
}
|
|
51
89
|
interface I18nLocale {
|
|
52
90
|
path: string;
|
|
53
91
|
}
|
|
92
|
+
/** The Vite plugin that serves the site's content declaration under {@link CONTENT_MODULE_ID}. */
|
|
54
93
|
interface ContentModulePlugin {
|
|
55
94
|
name: string;
|
|
56
95
|
resolveId(id: string): string | undefined;
|
|
@@ -114,12 +153,38 @@ interface SuiteOpIntegration {
|
|
|
114
153
|
'astro:build:done': (options: BuildDoneOptions) => Promise<void>;
|
|
115
154
|
};
|
|
116
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* `astro dev` proxy for `suiteop dev`: the Storefront API and the checkout bundle answer on the
|
|
158
|
+
* same paths as on a SuiteOp-hosted site, so site code has no dev-only branch. Enabled only when
|
|
159
|
+
* `SUITEOP_DEV_PROXY=1` (the CLI sets it with the two origins).
|
|
160
|
+
*
|
|
161
|
+
* API calls are sent with the site's own origin (`SUITEOP_EDGE_ORIGIN`), which is among its
|
|
162
|
+
* key's allowed origins: the API refuses a call from `http://localhost:<port>` (the site's
|
|
163
|
+
* server code) and one with no `Origin` at all (a same-origin GET from the browser).
|
|
164
|
+
*/
|
|
117
165
|
declare function devProxy(env: Record<string, string | undefined>): Record<string, ProxyTarget>;
|
|
118
166
|
interface SuiteOpIntegrationOptions {
|
|
167
|
+
/**
|
|
168
|
+
* The site's content model (`defineContent`), e.g. `'./src/suiteop.content.ts'`, relative to the
|
|
169
|
+
* project root. The build emits it at `/_suiteop/content-model.json` and the deploy registers it,
|
|
170
|
+
* so the dashboard shows a form for each type.
|
|
171
|
+
*/
|
|
119
172
|
content?: string;
|
|
173
|
+
/**
|
|
174
|
+
* The source of on-demand routes the conventions do not cover, for the sitemap:
|
|
175
|
+
* `{ '/blog/[slug]': 'collection:blog', '/things-to-do/[id]': 'guides', '/blog': 'page' }`.
|
|
176
|
+
* `page` lists a route without a parameter as one page. `/listing/[…]`, `/building/[…]` and
|
|
177
|
+
* `/area/[…]` are inferred.
|
|
178
|
+
*/
|
|
120
179
|
routes?: Record<string, SiteManifestSource>;
|
|
121
180
|
}
|
|
181
|
+
/** The Vite plugin re-exporting the site's content declaration at `file`. */
|
|
122
182
|
declare function contentModulePlugin(file: string): ContentModulePlugin;
|
|
183
|
+
/**
|
|
184
|
+
* The SuiteOp Astro integration: emits the site manifest at `/_suiteop/manifest.json` after a
|
|
185
|
+
* build, the site's content model at `/_suiteop/content-model.json` when it declares one, and
|
|
186
|
+
* wires the `suiteop dev` proxy.
|
|
187
|
+
*/
|
|
123
188
|
declare function suiteop(options?: SuiteOpIntegrationOptions): SuiteOpIntegration;
|
|
124
189
|
|
|
125
190
|
export { type BuildSiteManifestInput, CONTENT_MODEL_PATH, type ContentModulePlugin, SITE_MANIFEST_PATH, type SiteContext, type SiteManifest, type SiteManifestPage, type SiteManifestSource, type SuiteOpIntegration, type SuiteOpIntegrationOptions, buildSiteManifest, contentModulePlugin, devProxy, getSiteContext, suiteop };
|
package/dist/astro.d.ts
CHANGED
|
@@ -1,23 +1,45 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The site manifest a storefront build emits at `/_suiteop/manifest.json`. The platform reads it
|
|
3
|
+
* after a deploy for the sitemap and search features: which prerendered pages exist (with their
|
|
4
|
+
* title, language and last change) and which on-demand routes expand from catalogue data.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Where a dynamic route's pages come from: the listings, buildings or areas (`locations`) of the
|
|
8
|
+
* catalogue, the guides the site shows (`guides`), or the published items of one of the site's
|
|
9
|
+
* collections (`collection:<key>`). `page` is a server-rendered route without a parameter
|
|
10
|
+
* (`/blog`): the sitemap lists it as one page.
|
|
11
|
+
*/
|
|
12
|
+
type SiteManifestSource = 'listings' | 'buildings' | 'locations' | 'guides' | 'page' | `collection:${string}`;
|
|
2
13
|
interface SiteManifestPage {
|
|
14
|
+
/** Site-relative, no trailing slash (`/` for the home page). */
|
|
3
15
|
path: string;
|
|
4
16
|
locale: string;
|
|
5
17
|
title: string;
|
|
18
|
+
/** ISO 8601. */
|
|
6
19
|
lastmod: string;
|
|
7
20
|
}
|
|
8
21
|
interface SiteManifest {
|
|
9
22
|
version: 1;
|
|
10
23
|
generatedAt: string;
|
|
11
24
|
pages: SiteManifestPage[];
|
|
25
|
+
/** On-demand routes whose pages are one per catalogue record. Checkout, confirmation and search never appear. */
|
|
12
26
|
dynamicRoutes: {
|
|
13
27
|
pattern: string;
|
|
14
28
|
source: SiteManifestSource;
|
|
15
29
|
}[];
|
|
16
30
|
}
|
|
17
31
|
interface BuildSiteManifestInput {
|
|
32
|
+
/** Pathnames of the prerendered pages, as the build reports them. */
|
|
18
33
|
pages: string[];
|
|
34
|
+
/** Route patterns of the on-demand (server-rendered) routes, e.g. `/listing/[id]`. */
|
|
19
35
|
dynamicRoutes: string[];
|
|
36
|
+
/**
|
|
37
|
+
* Sources the site declares for routes the conventions do not cover
|
|
38
|
+
* (`suiteop({ routes: { '/blog/[slug]': 'collection:blog' } })`). Only on-demand routes count:
|
|
39
|
+
* a prerendered page is listed as a page already.
|
|
40
|
+
*/
|
|
20
41
|
declaredRoutes?: Record<string, SiteManifestSource>;
|
|
42
|
+
/** The built HTML for a page pathname; null when it cannot be read. */
|
|
21
43
|
readHtml(pathname: string): Promise<string | null>;
|
|
22
44
|
now: Date;
|
|
23
45
|
config: {
|
|
@@ -27,30 +49,47 @@ interface BuildSiteManifestInput {
|
|
|
27
49
|
}
|
|
28
50
|
declare function buildSiteManifest(input: BuildSiteManifestInput): Promise<SiteManifest>;
|
|
29
51
|
|
|
52
|
+
/**
|
|
53
|
+
* What the SuiteOp edge hands every request of a SuiteOp-hosted site on the Worker's
|
|
54
|
+
* `ctx.props` (`Astro.locals.cfContext.props` with `@astrojs/cloudflare`).
|
|
55
|
+
*/
|
|
30
56
|
interface SiteContext {
|
|
31
57
|
siteId: string;
|
|
32
58
|
orgId: string;
|
|
33
59
|
region: 'us' | 'eu' | 'apac';
|
|
34
60
|
environment: 'live' | 'test';
|
|
61
|
+
/** This environment's publishable key. Public by design — safe in HTML. */
|
|
35
62
|
publishableKey: string;
|
|
63
|
+
/** The visitor's country (ISO 3166-1 alpha-2) when the edge knows it. */
|
|
36
64
|
country: string | null;
|
|
65
|
+
/** Same-origin Storefront API path for the browser, proxied by the edge. */
|
|
37
66
|
apiBasePath: string;
|
|
67
|
+
/**
|
|
68
|
+
* Absolute Storefront API base for server-side calls (the site's regional API). Null when the
|
|
69
|
+
* edge does not provide one; server calls then fall back to the same-origin path.
|
|
70
|
+
*/
|
|
38
71
|
apiBaseUrl: string | null;
|
|
72
|
+
/** Headers the edge signs for THIS request (the visitor's IP); send them on server-side API calls. */
|
|
39
73
|
apiHeaders: Record<string, string>;
|
|
40
74
|
}
|
|
75
|
+
/** The edge's props for this request, or null when the site is not served by the SuiteOp edge. */
|
|
41
76
|
declare function getSiteContext(locals: object): SiteContext | null;
|
|
42
77
|
|
|
78
|
+
/** Where the manifest lands in the client build output, and the path the site serves it on. */
|
|
43
79
|
declare const SITE_MANIFEST_PATH = "_suiteop/manifest.json";
|
|
80
|
+
/** Where the site's content model lands in the client build output (`suiteop({ content })`). */
|
|
44
81
|
declare const CONTENT_MODEL_PATH = "_suiteop/content-model.json";
|
|
45
82
|
interface ProxyTarget {
|
|
46
83
|
target: string;
|
|
47
84
|
changeOrigin: boolean;
|
|
85
|
+
/** Set on every proxied request, replacing what the caller sent. */
|
|
48
86
|
headers?: Record<string, string>;
|
|
49
87
|
rewrite?: (path: string) => string;
|
|
50
88
|
}
|
|
51
89
|
interface I18nLocale {
|
|
52
90
|
path: string;
|
|
53
91
|
}
|
|
92
|
+
/** The Vite plugin that serves the site's content declaration under {@link CONTENT_MODULE_ID}. */
|
|
54
93
|
interface ContentModulePlugin {
|
|
55
94
|
name: string;
|
|
56
95
|
resolveId(id: string): string | undefined;
|
|
@@ -114,12 +153,38 @@ interface SuiteOpIntegration {
|
|
|
114
153
|
'astro:build:done': (options: BuildDoneOptions) => Promise<void>;
|
|
115
154
|
};
|
|
116
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* `astro dev` proxy for `suiteop dev`: the Storefront API and the checkout bundle answer on the
|
|
158
|
+
* same paths as on a SuiteOp-hosted site, so site code has no dev-only branch. Enabled only when
|
|
159
|
+
* `SUITEOP_DEV_PROXY=1` (the CLI sets it with the two origins).
|
|
160
|
+
*
|
|
161
|
+
* API calls are sent with the site's own origin (`SUITEOP_EDGE_ORIGIN`), which is among its
|
|
162
|
+
* key's allowed origins: the API refuses a call from `http://localhost:<port>` (the site's
|
|
163
|
+
* server code) and one with no `Origin` at all (a same-origin GET from the browser).
|
|
164
|
+
*/
|
|
117
165
|
declare function devProxy(env: Record<string, string | undefined>): Record<string, ProxyTarget>;
|
|
118
166
|
interface SuiteOpIntegrationOptions {
|
|
167
|
+
/**
|
|
168
|
+
* The site's content model (`defineContent`), e.g. `'./src/suiteop.content.ts'`, relative to the
|
|
169
|
+
* project root. The build emits it at `/_suiteop/content-model.json` and the deploy registers it,
|
|
170
|
+
* so the dashboard shows a form for each type.
|
|
171
|
+
*/
|
|
119
172
|
content?: string;
|
|
173
|
+
/**
|
|
174
|
+
* The source of on-demand routes the conventions do not cover, for the sitemap:
|
|
175
|
+
* `{ '/blog/[slug]': 'collection:blog', '/things-to-do/[id]': 'guides', '/blog': 'page' }`.
|
|
176
|
+
* `page` lists a route without a parameter as one page. `/listing/[…]`, `/building/[…]` and
|
|
177
|
+
* `/area/[…]` are inferred.
|
|
178
|
+
*/
|
|
120
179
|
routes?: Record<string, SiteManifestSource>;
|
|
121
180
|
}
|
|
181
|
+
/** The Vite plugin re-exporting the site's content declaration at `file`. */
|
|
122
182
|
declare function contentModulePlugin(file: string): ContentModulePlugin;
|
|
183
|
+
/**
|
|
184
|
+
* The SuiteOp Astro integration: emits the site manifest at `/_suiteop/manifest.json` after a
|
|
185
|
+
* build, the site's content model at `/_suiteop/content-model.json` when it declares one, and
|
|
186
|
+
* wires the `suiteop dev` proxy.
|
|
187
|
+
*/
|
|
123
188
|
declare function suiteop(options?: SuiteOpIntegrationOptions): SuiteOpIntegration;
|
|
124
189
|
|
|
125
190
|
export { type BuildSiteManifestInput, CONTENT_MODEL_PATH, type ContentModulePlugin, SITE_MANIFEST_PATH, type SiteContext, type SiteManifest, type SiteManifestPage, type SiteManifestSource, type SuiteOpIntegration, type SuiteOpIntegrationOptions, buildSiteManifest, contentModulePlugin, devProxy, getSiteContext, suiteop };
|
package/dist/astro.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
-
var SOURCES=[[/^\/listing\/\[[^\]/]+\]$/,"listings"],[/^\/building\/\[[^\]/]+\]$/,"buildings"],[/^\/area\/\[[^\]/]+\]$/,"locations"]];var DECLARABLE_SOURCE=/^(listings|buildings|locations|collection:[a-z][a-z0-9_]{0,39})$/;var ERROR_PAGES=new Set(["404","500"]);function normalizePath(pathname){const withSlash=pathname.startsWith("/")?pathname:`/${pathname}`;const trimmed=withSlash.replace(/\/+$/,"").replace(/\/index(\.html)?$/,"");return trimmed===""?"/":trimmed}var ENTITIES={amp:"&",lt:"<",gt:">",quot:'"',apos:"'"};function decodeEntities(text){return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi,(whole,ref)=>{if(ref[0]==="#"){const code=ref[1]==="x"||ref[1]==="X"?parseInt(ref.slice(2),16):Number(ref.slice(1));return Number.isFinite(code)&&code>0&&code<=1114111?String.fromCodePoint(code):whole}return ENTITIES[ref.toLowerCase()]??whole})}function metaContent(html,name){for(const[tag]of html.matchAll(/<meta\b[^>]*>/gi)){const attr=key=>new RegExp(`\\b${key}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`,"i").exec(tag);const tagName=attr("name");if((tagName?.[1]??tagName?.[2])?.toLowerCase()!==name)continue;const content=attr("content");return decodeEntities(content?.[1]??content?.[2]??"")}return null}function localeOf(path,config){const first=path.split("/")[1]??"";return first!==config.defaultLocale&&config.locales.includes(first)?first:config.defaultLocale}function isErrorPage(path,locale,config){const rest=locale===config.defaultLocale?path:path.slice(locale.length+1);return ERROR_PAGES.has(rest.replace(/^\//,""))}async function buildSiteManifest(input){const now=input.now.toISOString();const pages=[];const seen=new Set;for(const pathname of input.pages){const path=normalizePath(pathname);if(seen.has(path))continue;seen.add(path);const locale=localeOf(path,input.config);if(isErrorPage(path,locale,input.config))continue;const html=await input.readHtml(pathname);if(!html)continue;if(/\bnoindex\b/i.test(metaContent(html,"robots")??""))continue;const title=/<title[^>]*>([\s\S]*?)<\/title>/i.exec(html)?.[1];const lastmodRaw=metaContent(html,"suiteop:lastmod");const lastmodMs=lastmodRaw?Date.parse(lastmodRaw):Number.NaN;pages.push({path,locale,title:title?decodeEntities(title.trim()):"",lastmod:Number.isNaN(lastmodMs)?now:new Date(lastmodMs).toISOString()});}pages.sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0);const dynamicRoutes=[];for(const pattern of input.dynamicRoutes){const source=input.declaredRoutes?.[pattern]??SOURCES.find(([re])=>re.test(pattern))?.[1];if(source&&!dynamicRoutes.some(r=>r.pattern===pattern)){dynamicRoutes.push({pattern,source});}}return {version:1,generatedAt:now,pages,dynamicRoutes}}var sameOriginPath=z.string().regex(/^\/(?![/\\])/);var siteContextSchema=z.object({siteId:z.string().min(1),orgId:z.string().min(1),region:z.enum(["us","eu","apac"]),environment:z.enum(["live","test"]),publishableKey:z.string().regex(/^pk_(live|test)_/),country:z.string().nullable().optional(),apiBasePath:sameOriginPath,apiBaseUrl:z.url({protocol:/^https$/}).nullable().optional(),apiHeaders:z.record(z.string().regex(/^x-suiteop-[a-z0-9-]+$/),z.string()).optional()});function getSiteContext(locals){const props=locals.cfContext?.props;if(typeof props!=="object"||props===null)return null;const parsed=siteContextSchema.safeParse(props);if(!parsed.success)return null;const p=parsed.data;return {...p,country:p.country??null,apiBaseUrl:p.apiBaseUrl??null,apiHeaders:p.apiHeaders??{}}}var SITE_MANIFEST_PATH="_suiteop/manifest.json";var CONTENT_MODEL_PATH="_suiteop/content-model.json";var CONTENT_MODULE_ID="virtual:suiteop/content";var RESOLVED_CONTENT_MODULE_ID=`\0${CONTENT_MODULE_ID}`;function devProxy(env){const apiOrigin=env["SUITEOP_API_ORIGIN"];const edgeOrigin=env["SUITEOP_EDGE_ORIGIN"];const proxy={};if(apiOrigin){proxy["/api/storefront/v1"]={target:apiOrigin,changeOrigin:true,...edgeOrigin?{headers:{origin:edgeOrigin}}:{}};}if(edgeOrigin){proxy["^/checkout/(manifest\\.json|v\\d+(\\.[0-9a-f]+)?\\.js)$"]={target:edgeOrigin,changeOrigin:true};}return proxy}function contentModulePlugin(file){return {name:"@suiteop/storefront:content",resolveId:id=>id===CONTENT_MODULE_ID?RESOLVED_CONTENT_MODULE_ID:void 0,load:id=>id===RESOLVED_CONTENT_MODULE_ID?`export { default } from ${JSON.stringify(file)}`:void 0}}function suiteop(options={}){for(const[pattern,source]of Object.entries(options.routes??{})){if(!DECLARABLE_SOURCE.test(source)){throw new Error(`suiteop(): route ${pattern} has source "${source}"; use listings, buildings, locations or collection:<key>.`)}}let i18n={locales:[],defaultLocale:"en"};let format="directory";let onDemandRoutes=[];return {name:"@suiteop/storefront",hooks:{"astro:config:setup":({command,config,updateConfig,injectRoute})=>{if(options.content){const file=decodeURIComponent(new URL(options.content,config.root).pathname);updateConfig({vite:{plugins:[contentModulePlugin(file)]}});injectRoute({pattern:`/${CONTENT_MODEL_PATH}`,entrypoint:"@suiteop/storefront/astro/content-model",prerender:true});}if(command==="dev"&&process.env["SUITEOP_DEV_PROXY"]==="1"){updateConfig({vite:{server:{proxy:devProxy(process.env),strictPort:true}}});}},"astro:config:done":({config})=>{format=config.build.format;if(config.i18n){i18n={locales:config.i18n.locales.map(l=>typeof l==="string"?l:l.path),defaultLocale:config.i18n.defaultLocale};}},"astro:routes:resolved":({routes})=>{onDemandRoutes=routes.filter(r=>r.type==="page"&&!r.isPrerendered&&r.origin==="project").map(r=>r.pattern);},"astro:build:done":async({dir,pages,logger})=>{const{mkdir,writeFile}=await import('node:fs/promises');const built=await builtHtmlPaths(dir);const all=new Set([...pages.map(p=>`/${p.pathname}`),...built]);const manifest=await buildSiteManifest({pages:[...all],dynamicRoutes:onDemandRoutes,declaredRoutes:options.routes,readHtml:pathname=>readBuiltHtml(dir,pathname,format),now:new Date,config:i18n});const target=new URL(SITE_MANIFEST_PATH,dir);await mkdir(new URL(".",target),{recursive:true});await writeFile(target,`${JSON.stringify(manifest)}
|
|
3
|
+
var SOURCES=[[/^\/listing\/\[[^\]/]+\]$/,"listings"],[/^\/building\/\[[^\]/]+\]$/,"buildings"],[/^\/area\/\[[^\]/]+\]$/,"locations"]];var DECLARABLE_SOURCE=/^(listings|buildings|locations|guides|page|collection:[a-z][a-z0-9_]{0,39})$/;var ERROR_PAGES=new Set(["404","500"]);function normalizePath(pathname){const withSlash=pathname.startsWith("/")?pathname:`/${pathname}`;const trimmed=withSlash.replace(/\/+$/,"").replace(/\/index(\.html)?$/,"");return trimmed===""?"/":trimmed}var ENTITIES={amp:"&",lt:"<",gt:">",quot:'"',apos:"'"};function decodeEntities(text){return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi,(whole,ref)=>{if(ref[0]==="#"){const code=ref[1]==="x"||ref[1]==="X"?parseInt(ref.slice(2),16):Number(ref.slice(1));return Number.isFinite(code)&&code>0&&code<=1114111?String.fromCodePoint(code):whole}return ENTITIES[ref.toLowerCase()]??whole})}function metaContent(html,name){for(const[tag]of html.matchAll(/<meta\b[^>]*>/gi)){const attr=key=>new RegExp(`\\b${key}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`,"i").exec(tag);const tagName=attr("name");if((tagName?.[1]??tagName?.[2])?.toLowerCase()!==name)continue;const content=attr("content");return decodeEntities(content?.[1]??content?.[2]??"")}return null}function localeOf(path,config){const first=path.split("/")[1]??"";return first!==config.defaultLocale&&config.locales.includes(first)?first:config.defaultLocale}function isErrorPage(path,locale,config){const rest=locale===config.defaultLocale?path:path.slice(locale.length+1);return ERROR_PAGES.has(rest.replace(/^\//,""))}async function buildSiteManifest(input){const now=input.now.toISOString();const pages=[];const seen=new Set;for(const pathname of input.pages){const path=normalizePath(pathname);if(seen.has(path))continue;seen.add(path);const locale=localeOf(path,input.config);if(isErrorPage(path,locale,input.config))continue;const html=await input.readHtml(pathname);if(!html)continue;if(/\bnoindex\b/i.test(metaContent(html,"robots")??""))continue;const title=/<title[^>]*>([\s\S]*?)<\/title>/i.exec(html)?.[1];const lastmodRaw=metaContent(html,"suiteop:lastmod");const lastmodMs=lastmodRaw?Date.parse(lastmodRaw):Number.NaN;pages.push({path,locale,title:title?decodeEntities(title.trim()):"",lastmod:Number.isNaN(lastmodMs)?now:new Date(lastmodMs).toISOString()});}pages.sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0);const dynamicRoutes=[];for(const pattern of input.dynamicRoutes){const source=input.declaredRoutes?.[pattern]??SOURCES.find(([re])=>re.test(pattern))?.[1];if(source&&!dynamicRoutes.some(r=>r.pattern===pattern)){dynamicRoutes.push({pattern,source});}}return {version:1,generatedAt:now,pages,dynamicRoutes}}var sameOriginPath=z.string().regex(/^\/(?![/\\])/);var siteContextSchema=z.object({siteId:z.string().min(1),orgId:z.string().min(1),region:z.enum(["us","eu","apac"]),environment:z.enum(["live","test"]),publishableKey:z.string().regex(/^pk_(live|test)_/),country:z.string().nullable().optional(),apiBasePath:sameOriginPath,apiBaseUrl:z.url({protocol:/^https$/}).nullable().optional(),apiHeaders:z.record(z.string().regex(/^x-suiteop-[a-z0-9-]+$/),z.string()).optional()});function getSiteContext(locals){const props=locals.cfContext?.props;if(typeof props!=="object"||props===null)return null;const parsed=siteContextSchema.safeParse(props);if(!parsed.success)return null;const p=parsed.data;return {...p,country:p.country??null,apiBaseUrl:p.apiBaseUrl??null,apiHeaders:p.apiHeaders??{}}}var SITE_MANIFEST_PATH="_suiteop/manifest.json";var CONTENT_MODEL_PATH="_suiteop/content-model.json";var CONTENT_MODULE_ID="virtual:suiteop/content";var RESOLVED_CONTENT_MODULE_ID=`\0${CONTENT_MODULE_ID}`;function devProxy(env){const apiOrigin=env["SUITEOP_API_ORIGIN"];const edgeOrigin=env["SUITEOP_EDGE_ORIGIN"];const proxy={};if(apiOrigin){proxy["/api/storefront/v1"]={target:apiOrigin,changeOrigin:true,...edgeOrigin?{headers:{origin:edgeOrigin}}:{}};}if(edgeOrigin){proxy["^/checkout/(manifest\\.json|v\\d+(\\.[0-9a-f]+)?\\.js)$"]={target:edgeOrigin,changeOrigin:true};}return proxy}function contentModulePlugin(file){return {name:"@suiteop/storefront:content",resolveId:id=>id===CONTENT_MODULE_ID?RESOLVED_CONTENT_MODULE_ID:void 0,load:id=>id===RESOLVED_CONTENT_MODULE_ID?`export { default } from ${JSON.stringify(file)}`:void 0}}function suiteop(options={}){for(const[pattern,source]of Object.entries(options.routes??{})){if(!DECLARABLE_SOURCE.test(source)){throw new Error(`suiteop(): route ${pattern} has source "${source}"; use listings, buildings, locations, guides, page or collection:<key>.`)}if(source==="page"&&pattern.includes("[")){throw new Error(`suiteop(): route ${pattern} has a parameter, so it is not one page.`)}}let i18n={locales:[],defaultLocale:"en"};let format="directory";let onDemandRoutes=[];return {name:"@suiteop/storefront",hooks:{"astro:config:setup":({command,config,updateConfig,injectRoute})=>{if(options.content){const file=decodeURIComponent(new URL(options.content,config.root).pathname);updateConfig({vite:{plugins:[contentModulePlugin(file)]}});injectRoute({pattern:`/${CONTENT_MODEL_PATH}`,entrypoint:"@suiteop/storefront/astro/content-model",prerender:true});}if(command==="dev"&&process.env["SUITEOP_DEV_PROXY"]==="1"){updateConfig({vite:{server:{proxy:devProxy(process.env),strictPort:true}}});}},"astro:config:done":({config})=>{format=config.build.format;if(config.i18n){i18n={locales:config.i18n.locales.map(l=>typeof l==="string"?l:l.path),defaultLocale:config.i18n.defaultLocale};}},"astro:routes:resolved":({routes})=>{onDemandRoutes=routes.filter(r=>r.type==="page"&&!r.isPrerendered&&r.origin==="project").map(r=>r.pattern);},"astro:build:done":async({dir,pages,logger})=>{const{mkdir,writeFile}=await import('node:fs/promises');const built=await builtHtmlPaths(dir);const all=new Set([...pages.map(p=>`/${p.pathname}`),...built]);const manifest=await buildSiteManifest({pages:[...all],dynamicRoutes:onDemandRoutes,declaredRoutes:options.routes,readHtml:pathname=>readBuiltHtml(dir,pathname,format),now:new Date,config:i18n});const target=new URL(SITE_MANIFEST_PATH,dir);await mkdir(new URL(".",target),{recursive:true});await writeFile(target,`${JSON.stringify(manifest)}
|
|
4
4
|
`);logger.info(`suiteop manifest: ${manifest.pages.length} pages, ${manifest.dynamicRoutes.length} dynamic routes`);}}}}async function builtHtmlPaths(dir){const{readdir}=await import('node:fs/promises');const{fileURLToPath}=await import('node:url');const entries=await readdir(fileURLToPath(dir),{recursive:true,withFileTypes:true});const paths=[];for(const entry of entries){if(!entry.isFile()||!entry.name.endsWith(".html"))continue;const rel=`${entry.parentPath}/${entry.name}`.slice(fileURLToPath(dir).length).replace(/\\/g,"/").replace(/^\/+/,"");if(/^(_astro|_suiteop)\//.test(rel))continue;paths.push(`/${rel.replace(/(^|\/)index\.html$/,"$1").replace(/\.html$/,"")}`);}return paths}async function readBuiltHtml(dir,pathname,format){const{readFile}=await import('node:fs/promises');const base=pathname.replace(/^\/+/,"").replace(/\/+$/,"");const candidates=base===""?["index.html"]:format==="file"?[`${base}.html`,`${base}/index.html`]:[`${base}/index.html`,`${base}.html`];for(const candidate of candidates){try{return await readFile(new URL(candidate,dir),"utf8")}catch(err){if(err.code!=="ENOENT")throw err}}return null}
|
|
5
5
|
|
|
6
6
|
export { CONTENT_MODEL_PATH, SITE_MANIFEST_PATH, buildSiteManifest, contentModulePlugin, devProxy, getSiteContext, suiteop };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
declare const storefrontStaySchema: z.ZodObject<{
|
|
4
|
+
checkIn: z.ZodISODate;
|
|
5
|
+
checkOut: z.ZodISODate;
|
|
6
|
+
guests: z.ZodNumber;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
type StorefrontStay = z.infer<typeof storefrontStaySchema>;
|
|
9
|
+
declare const storefrontCalendarQuerySchema: z.ZodObject<{
|
|
10
|
+
from: z.ZodISODate;
|
|
11
|
+
to: z.ZodISODate;
|
|
12
|
+
}, z.core.$strip>;
|
|
13
|
+
type StorefrontCalendarQuery = z.infer<typeof storefrontCalendarQuerySchema>;
|
|
14
|
+
declare const storefrontCalendarDaySchema: z.ZodObject<{
|
|
15
|
+
date: z.ZodISODate;
|
|
16
|
+
priceMinor: z.ZodNullable<z.ZodNumber>;
|
|
17
|
+
currency: z.ZodNullable<z.ZodString>;
|
|
18
|
+
minNights: z.ZodNullable<z.ZodNumber>;
|
|
19
|
+
closedToArrival: z.ZodOptional<z.ZodBoolean>;
|
|
20
|
+
closedToDeparture: z.ZodOptional<z.ZodBoolean>;
|
|
21
|
+
status: z.ZodType<"available" | "blocked" | "booked" | (string & {}), unknown, z.core.$ZodTypeInternals<"available" | "blocked" | "booked" | (string & {}), unknown>>;
|
|
22
|
+
}, z.core.$strip>;
|
|
23
|
+
type StorefrontCalendarDay = z.infer<typeof storefrontCalendarDaySchema>;
|
|
24
|
+
declare const storefrontCalendarSchema: z.ZodObject<{
|
|
25
|
+
listingId: z.ZodUUID;
|
|
26
|
+
bookable: z.ZodBoolean;
|
|
27
|
+
days: z.ZodArray<z.ZodObject<{
|
|
28
|
+
date: z.ZodISODate;
|
|
29
|
+
priceMinor: z.ZodNullable<z.ZodNumber>;
|
|
30
|
+
currency: z.ZodNullable<z.ZodString>;
|
|
31
|
+
minNights: z.ZodNullable<z.ZodNumber>;
|
|
32
|
+
closedToArrival: z.ZodOptional<z.ZodBoolean>;
|
|
33
|
+
closedToDeparture: z.ZodOptional<z.ZodBoolean>;
|
|
34
|
+
status: z.ZodType<"available" | "blocked" | "booked" | (string & {}), unknown, z.core.$ZodTypeInternals<"available" | "blocked" | "booked" | (string & {}), unknown>>;
|
|
35
|
+
}, z.core.$strip>>;
|
|
36
|
+
}, z.core.$strip>;
|
|
37
|
+
type StorefrontCalendar = z.infer<typeof storefrontCalendarSchema>;
|
|
38
|
+
declare const storefrontUnavailableReasonSchema: z.ZodEnum<{
|
|
39
|
+
closed_to_arrival: "closed_to_arrival";
|
|
40
|
+
closed_to_departure: "closed_to_departure";
|
|
41
|
+
dates_unavailable: "dates_unavailable";
|
|
42
|
+
max_guests: "max_guests";
|
|
43
|
+
min_nights: "min_nights";
|
|
44
|
+
not_bookable: "not_bookable";
|
|
45
|
+
}>;
|
|
46
|
+
type StorefrontUnavailableReason = z.infer<typeof storefrontUnavailableReasonSchema>;
|
|
47
|
+
declare const storefrontAvailabilitySchema: z.ZodObject<{
|
|
48
|
+
listingId: z.ZodUUID;
|
|
49
|
+
checkIn: z.ZodISODate;
|
|
50
|
+
checkOut: z.ZodISODate;
|
|
51
|
+
guests: z.ZodNumber;
|
|
52
|
+
bookable: z.ZodBoolean;
|
|
53
|
+
available: z.ZodBoolean;
|
|
54
|
+
reason: z.ZodNullable<z.ZodType<"closed_to_arrival" | "closed_to_departure" | "dates_unavailable" | "max_guests" | "min_nights" | "not_bookable" | (string & {}), unknown, z.core.$ZodTypeInternals<"closed_to_arrival" | "closed_to_departure" | "dates_unavailable" | "max_guests" | "min_nights" | "not_bookable" | (string & {}), unknown>>>;
|
|
55
|
+
minNights: z.ZodNullable<z.ZodNumber>;
|
|
56
|
+
maxGuests: z.ZodNullable<z.ZodNumber>;
|
|
57
|
+
}, z.core.$strip>;
|
|
58
|
+
type StorefrontAvailability = z.infer<typeof storefrontAvailabilitySchema>;
|
|
59
|
+
declare const storefrontBuildingAvailabilityQuerySchema: z.ZodObject<{
|
|
60
|
+
checkIn: z.ZodISODate;
|
|
61
|
+
checkOut: z.ZodISODate;
|
|
62
|
+
guests: z.ZodPipe<z.ZodCoercedNumber<unknown>, z.ZodNumber>;
|
|
63
|
+
limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
64
|
+
offset: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
65
|
+
}, z.core.$strip>;
|
|
66
|
+
type StorefrontBuildingAvailabilityQuery = z.infer<typeof storefrontBuildingAvailabilityQuerySchema>;
|
|
67
|
+
declare const STOREFRONT_UNIT_UNAVAILABLE_REASONS: readonly ["not_bookable", "dates_unavailable", "min_nights", "max_guests", "closed_to_arrival", "closed_to_departure", "calendar_unavailable"];
|
|
68
|
+
type StorefrontUnitUnavailableReason = (typeof STOREFRONT_UNIT_UNAVAILABLE_REASONS)[number];
|
|
69
|
+
declare const storefrontUnitAvailabilitySchema: z.ZodObject<{
|
|
70
|
+
listingId: z.ZodUUID;
|
|
71
|
+
available: z.ZodBoolean;
|
|
72
|
+
reason: z.ZodNullable<z.ZodType<"calendar_unavailable" | "closed_to_arrival" | "closed_to_departure" | "dates_unavailable" | "max_guests" | "min_nights" | "not_bookable" | (string & {}), unknown, z.core.$ZodTypeInternals<"calendar_unavailable" | "closed_to_arrival" | "closed_to_departure" | "dates_unavailable" | "max_guests" | "min_nights" | "not_bookable" | (string & {}), unknown>>>;
|
|
73
|
+
totalMinor: z.ZodNullable<z.ZodNumber>;
|
|
74
|
+
currency: z.ZodNullable<z.ZodString>;
|
|
75
|
+
nights: z.ZodNumber;
|
|
76
|
+
nextAvailable: z.ZodNullable<z.ZodObject<{
|
|
77
|
+
checkIn: z.ZodISODate;
|
|
78
|
+
checkOut: z.ZodISODate;
|
|
79
|
+
}, z.core.$strip>>;
|
|
80
|
+
maxGuests: z.ZodNullable<z.ZodNumber>;
|
|
81
|
+
}, z.core.$strip>;
|
|
82
|
+
type StorefrontUnitAvailability = z.infer<typeof storefrontUnitAvailabilitySchema>;
|
|
83
|
+
|
|
84
|
+
export type { StorefrontAvailability as S, StorefrontBuildingAvailabilityQuery as a, StorefrontCalendar as b, StorefrontCalendarDay as c, StorefrontCalendarQuery as d, StorefrontStay as e, StorefrontUnavailableReason as f, StorefrontUnitAvailability as g, StorefrontUnitUnavailableReason as h };
|