@suiteop/storefront 0.5.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +4 -2
  3. package/dist/astro-content-model.cjs +3 -3
  4. package/dist/astro-content-model.js +2 -2
  5. package/dist/astro.cjs +1 -1
  6. package/dist/astro.d.cts +66 -1
  7. package/dist/astro.d.ts +66 -1
  8. package/dist/astro.js +1 -1
  9. package/dist/{availability-Dh0yLSkj.d.cts → availability.d-Cla9GuGc.d.cts} +8 -8
  10. package/dist/{availability-Dh0yLSkj.d.ts → availability.d-Cla9GuGc.d.ts} +8 -8
  11. package/dist/checkout.d.cts +11 -0
  12. package/dist/checkout.d.ts +11 -0
  13. package/dist/{chunk-IUNIQSFB.js → chunk-3B6ZYRYF.js} +1 -1
  14. package/dist/chunk-3NE27PHL.cjs +19 -0
  15. package/dist/chunk-JF245QUY.cjs +9 -0
  16. package/dist/{chunk-NNWJZUQL.cjs → chunk-OXCLLFCY.cjs} +2 -2
  17. package/dist/chunk-QBHOJGZI.cjs +21 -0
  18. package/dist/chunk-UQWQPTHO.js +3 -0
  19. package/dist/chunk-XOCFEVP4.js +6 -0
  20. package/dist/chunk-YIYGBTAM.js +9 -0
  21. package/dist/{client-C9Igm7O-.d.ts → client-CX8K7JNt.d.cts} +65 -11
  22. package/dist/{client-2KyVv-2l.d.cts → client-s-lEwqzc.d.ts} +65 -11
  23. package/dist/components.d.cts +43 -1
  24. package/dist/components.d.ts +43 -1
  25. package/dist/config.cjs +3 -3
  26. package/dist/config.d.cts +78 -74
  27. package/dist/config.d.ts +78 -74
  28. package/dist/config.js +2 -2
  29. package/dist/content.cjs +9 -9
  30. package/dist/content.d.cts +53 -9
  31. package/dist/content.d.ts +53 -9
  32. package/dist/content.js +2 -2
  33. package/dist/{errors-DpImb0Uf.d.cts → errors-Oax7djdp.d.ts} +54 -13
  34. package/dist/{errors-BGgJ--XR.d.ts → errors-welPvrD6.d.cts} +54 -13
  35. package/dist/index.cjs +12 -12
  36. package/dist/index.d.cts +27 -8
  37. package/dist/index.d.ts +27 -8
  38. package/dist/index.js +3 -3
  39. package/dist/react.cjs +4 -4
  40. package/dist/react.d.cts +24 -3
  41. package/dist/react.d.ts +24 -3
  42. package/dist/react.js +3 -3
  43. package/package.json +1 -1
  44. package/dist/chunk-5TUJX6GY.js +0 -5
  45. package/dist/chunk-LY73ZYN5.cjs +0 -20
  46. package/dist/chunk-NZWYOV5Y.cjs +0 -19
  47. package/dist/chunk-PYVUDQSQ.js +0 -3
  48. package/dist/chunk-YSKWSL2F.js +0 -9
  49. package/dist/chunk-ZLAOY6IF.cjs +0 -9
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
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
+
5
19
  ## 0.5.0
6
20
 
7
21
  - The site config takes optional `layout` (`listings`, the default, `locations`, `areas` or
package/README.md CHANGED
@@ -232,8 +232,10 @@ set from `colors.primary` — the `--so-*` colours, type scale, radii and button
232
232
 
233
233
  `suiteop()` writes `/_suiteop/manifest.json` after every build: the prerendered pages (path,
234
234
  language, title, last change — pages marked `noindex` are left out) and the on-demand routes that
235
- expand from catalogue data (`/listing/[id]`, `/building/[id]`). `/_suiteop/*` is reserved for the
236
- platform; do not add pages under it.
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.
237
239
 
238
240
  On SuiteOp hosting, `getSiteContext(Astro.locals)` (`@suiteop/storefront/astro`) returns the
239
241
  per-request site props the edge passes (publishable key, API bases; null elsewhere). The checkout
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- var chunkNNWJZUQL_cjs = require('./chunk-NNWJZUQL.cjs');
4
- require('./chunk-LY73ZYN5.cjs');
3
+ var chunkOXCLLFCY_cjs = require('./chunk-OXCLLFCY.cjs');
4
+ require('./chunk-QBHOJGZI.cjs');
5
5
  require('./chunk-J5FZ4WDQ.cjs');
6
6
  require('./chunk-EHQFWSP6.cjs');
7
7
  var definition = require('virtual:suiteop/content');
@@ -10,7 +10,7 @@ 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(chunkNNWJZUQL_cjs.parseContentModel(definition__default.default))}
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,5 +1,5 @@
1
- import { parseContentModel } from './chunk-IUNIQSFB.js';
2
- import './chunk-5TUJX6GY.js';
1
+ import { parseContentModel } from './chunk-3B6ZYRYF.js';
2
+ import './chunk-XOCFEVP4.js';
3
3
  import './chunk-W5K7IDYN.js';
4
4
  import './chunk-IBCBDAGY.js';
5
5
  import definition from 'virtual:suiteop/content';
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
- type SiteManifestSource = 'listings' | 'buildings' | 'locations' | `collection:${string}`;
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
- type SiteManifestSource = 'listings' | 'buildings' | 'locations' | `collection:${string}`;
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 };
@@ -18,7 +18,7 @@ declare const storefrontCalendarDaySchema: z.ZodObject<{
18
18
  minNights: z.ZodNullable<z.ZodNumber>;
19
19
  closedToArrival: z.ZodOptional<z.ZodBoolean>;
20
20
  closedToDeparture: z.ZodOptional<z.ZodBoolean>;
21
- status: z.ZodType<"blocked" | "available" | "booked" | (string & {}), unknown, z.core.$ZodTypeInternals<"blocked" | "available" | "booked" | (string & {}), unknown>>;
21
+ status: z.ZodType<"available" | "blocked" | "booked" | (string & {}), unknown, z.core.$ZodTypeInternals<"available" | "blocked" | "booked" | (string & {}), unknown>>;
22
22
  }, z.core.$strip>;
23
23
  type StorefrontCalendarDay = z.infer<typeof storefrontCalendarDaySchema>;
24
24
  declare const storefrontCalendarSchema: z.ZodObject<{
@@ -31,17 +31,17 @@ declare const storefrontCalendarSchema: z.ZodObject<{
31
31
  minNights: z.ZodNullable<z.ZodNumber>;
32
32
  closedToArrival: z.ZodOptional<z.ZodBoolean>;
33
33
  closedToDeparture: z.ZodOptional<z.ZodBoolean>;
34
- status: z.ZodType<"blocked" | "available" | "booked" | (string & {}), unknown, z.core.$ZodTypeInternals<"blocked" | "available" | "booked" | (string & {}), unknown>>;
34
+ status: z.ZodType<"available" | "blocked" | "booked" | (string & {}), unknown, z.core.$ZodTypeInternals<"available" | "blocked" | "booked" | (string & {}), unknown>>;
35
35
  }, z.core.$strip>>;
36
36
  }, z.core.$strip>;
37
37
  type StorefrontCalendar = z.infer<typeof storefrontCalendarSchema>;
38
38
  declare const storefrontUnavailableReasonSchema: z.ZodEnum<{
39
- not_bookable: "not_bookable";
40
- dates_unavailable: "dates_unavailable";
41
- min_nights: "min_nights";
42
- max_guests: "max_guests";
43
39
  closed_to_arrival: "closed_to_arrival";
44
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
45
  }>;
46
46
  type StorefrontUnavailableReason = z.infer<typeof storefrontUnavailableReasonSchema>;
47
47
  declare const storefrontAvailabilitySchema: z.ZodObject<{
@@ -51,7 +51,7 @@ declare const storefrontAvailabilitySchema: z.ZodObject<{
51
51
  guests: z.ZodNumber;
52
52
  bookable: z.ZodBoolean;
53
53
  available: z.ZodBoolean;
54
- reason: z.ZodNullable<z.ZodType<(string & {}) | "not_bookable" | "dates_unavailable" | "min_nights" | "max_guests" | "closed_to_arrival" | "closed_to_departure", unknown, z.core.$ZodTypeInternals<(string & {}) | "not_bookable" | "dates_unavailable" | "min_nights" | "max_guests" | "closed_to_arrival" | "closed_to_departure", unknown>>>;
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
55
  minNights: z.ZodNullable<z.ZodNumber>;
56
56
  maxGuests: z.ZodNullable<z.ZodNumber>;
57
57
  }, z.core.$strip>;
@@ -69,7 +69,7 @@ type StorefrontUnitUnavailableReason = (typeof STOREFRONT_UNIT_UNAVAILABLE_REASO
69
69
  declare const storefrontUnitAvailabilitySchema: z.ZodObject<{
70
70
  listingId: z.ZodUUID;
71
71
  available: z.ZodBoolean;
72
- reason: z.ZodNullable<z.ZodType<(string & {}) | "not_bookable" | "dates_unavailable" | "min_nights" | "max_guests" | "closed_to_arrival" | "closed_to_departure" | "calendar_unavailable", unknown, z.core.$ZodTypeInternals<(string & {}) | "not_bookable" | "dates_unavailable" | "min_nights" | "max_guests" | "closed_to_arrival" | "closed_to_departure" | "calendar_unavailable", unknown>>>;
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
73
  totalMinor: z.ZodNullable<z.ZodNumber>;
74
74
  currency: z.ZodNullable<z.ZodString>;
75
75
  nights: z.ZodNumber;
@@ -18,7 +18,7 @@ declare const storefrontCalendarDaySchema: z.ZodObject<{
18
18
  minNights: z.ZodNullable<z.ZodNumber>;
19
19
  closedToArrival: z.ZodOptional<z.ZodBoolean>;
20
20
  closedToDeparture: z.ZodOptional<z.ZodBoolean>;
21
- status: z.ZodType<"blocked" | "available" | "booked" | (string & {}), unknown, z.core.$ZodTypeInternals<"blocked" | "available" | "booked" | (string & {}), unknown>>;
21
+ status: z.ZodType<"available" | "blocked" | "booked" | (string & {}), unknown, z.core.$ZodTypeInternals<"available" | "blocked" | "booked" | (string & {}), unknown>>;
22
22
  }, z.core.$strip>;
23
23
  type StorefrontCalendarDay = z.infer<typeof storefrontCalendarDaySchema>;
24
24
  declare const storefrontCalendarSchema: z.ZodObject<{
@@ -31,17 +31,17 @@ declare const storefrontCalendarSchema: z.ZodObject<{
31
31
  minNights: z.ZodNullable<z.ZodNumber>;
32
32
  closedToArrival: z.ZodOptional<z.ZodBoolean>;
33
33
  closedToDeparture: z.ZodOptional<z.ZodBoolean>;
34
- status: z.ZodType<"blocked" | "available" | "booked" | (string & {}), unknown, z.core.$ZodTypeInternals<"blocked" | "available" | "booked" | (string & {}), unknown>>;
34
+ status: z.ZodType<"available" | "blocked" | "booked" | (string & {}), unknown, z.core.$ZodTypeInternals<"available" | "blocked" | "booked" | (string & {}), unknown>>;
35
35
  }, z.core.$strip>>;
36
36
  }, z.core.$strip>;
37
37
  type StorefrontCalendar = z.infer<typeof storefrontCalendarSchema>;
38
38
  declare const storefrontUnavailableReasonSchema: z.ZodEnum<{
39
- not_bookable: "not_bookable";
40
- dates_unavailable: "dates_unavailable";
41
- min_nights: "min_nights";
42
- max_guests: "max_guests";
43
39
  closed_to_arrival: "closed_to_arrival";
44
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
45
  }>;
46
46
  type StorefrontUnavailableReason = z.infer<typeof storefrontUnavailableReasonSchema>;
47
47
  declare const storefrontAvailabilitySchema: z.ZodObject<{
@@ -51,7 +51,7 @@ declare const storefrontAvailabilitySchema: z.ZodObject<{
51
51
  guests: z.ZodNumber;
52
52
  bookable: z.ZodBoolean;
53
53
  available: z.ZodBoolean;
54
- reason: z.ZodNullable<z.ZodType<(string & {}) | "not_bookable" | "dates_unavailable" | "min_nights" | "max_guests" | "closed_to_arrival" | "closed_to_departure", unknown, z.core.$ZodTypeInternals<(string & {}) | "not_bookable" | "dates_unavailable" | "min_nights" | "max_guests" | "closed_to_arrival" | "closed_to_departure", unknown>>>;
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
55
  minNights: z.ZodNullable<z.ZodNumber>;
56
56
  maxGuests: z.ZodNullable<z.ZodNumber>;
57
57
  }, z.core.$strip>;
@@ -69,7 +69,7 @@ type StorefrontUnitUnavailableReason = (typeof STOREFRONT_UNIT_UNAVAILABLE_REASO
69
69
  declare const storefrontUnitAvailabilitySchema: z.ZodObject<{
70
70
  listingId: z.ZodUUID;
71
71
  available: z.ZodBoolean;
72
- reason: z.ZodNullable<z.ZodType<(string & {}) | "not_bookable" | "dates_unavailable" | "min_nights" | "max_guests" | "closed_to_arrival" | "closed_to_departure" | "calendar_unavailable", unknown, z.core.$ZodTypeInternals<(string & {}) | "not_bookable" | "dates_unavailable" | "min_nights" | "max_guests" | "closed_to_arrival" | "closed_to_departure" | "calendar_unavailable", unknown>>>;
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
73
  totalMinor: z.ZodNullable<z.ZodNumber>;
74
74
  currency: z.ZodNullable<z.ZodString>;
75
75
  nights: z.ZodNumber;
@@ -1,7 +1,18 @@
1
1
  interface LoadCheckoutOptions {
2
+ /** The checkout bundle's major version. */
2
3
  major?: 1;
4
+ /**
5
+ * The storefront host that serves the bundle. Default: the page's own origin (SuiteOp-hosted
6
+ * sites). A self-hosted site passes its SuiteOp storefront host, e.g. `https://<site>.suiteop.site`.
7
+ */
3
8
  origin?: string;
4
9
  }
10
+ /**
11
+ * Loads the `<suiteop-checkout>` element: reads the storefront host's manifest, injects the
12
+ * bundle's content-hashed script with its Subresource Integrity hash, and resolves once the
13
+ * element is defined. Idempotent per origin + major; a no-op outside a browser. Rejects with a
14
+ * `StorefrontError` whose code is `checkout_unavailable`.
15
+ */
5
16
  declare function loadCheckout(options?: LoadCheckoutOptions): Promise<void>;
6
17
 
7
18
  export { type LoadCheckoutOptions, loadCheckout };
@@ -1,7 +1,18 @@
1
1
  interface LoadCheckoutOptions {
2
+ /** The checkout bundle's major version. */
2
3
  major?: 1;
4
+ /**
5
+ * The storefront host that serves the bundle. Default: the page's own origin (SuiteOp-hosted
6
+ * sites). A self-hosted site passes its SuiteOp storefront host, e.g. `https://<site>.suiteop.site`.
7
+ */
3
8
  origin?: string;
4
9
  }
10
+ /**
11
+ * Loads the `<suiteop-checkout>` element: reads the storefront host's manifest, injects the
12
+ * bundle's content-hashed script with its Subresource Integrity hash, and resolves once the
13
+ * element is defined. Idempotent per origin + major; a no-op outside a browser. Rejects with a
14
+ * `StorefrontError` whose code is `checkout_unavailable`.
15
+ */
5
16
  declare function loadCheckout(options?: LoadCheckoutOptions): Promise<void>;
6
17
 
7
18
  export { type LoadCheckoutOptions, loadCheckout };
@@ -1,4 +1,4 @@
1
- import { storefrontContentRefSchema, storefrontImageSchema } from './chunk-5TUJX6GY.js';
1
+ import { storefrontContentRefSchema, storefrontImageSchema } from './chunk-XOCFEVP4.js';
2
2
  import { StorefrontError } from './chunk-W5K7IDYN.js';
3
3
  import { z } from 'zod';
4
4