@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.
- package/CHANGELOG.md +14 -0
- package/README.md +4 -2
- package/dist/astro-content-model.cjs +3 -3
- package/dist/astro-content-model.js +2 -2
- 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-Dh0yLSkj.d.cts → availability.d-Cla9GuGc.d.cts} +8 -8
- package/dist/{availability-Dh0yLSkj.d.ts → availability.d-Cla9GuGc.d.ts} +8 -8
- package/dist/checkout.d.cts +11 -0
- package/dist/checkout.d.ts +11 -0
- package/dist/{chunk-IUNIQSFB.js → chunk-3B6ZYRYF.js} +1 -1
- package/dist/chunk-3NE27PHL.cjs +19 -0
- package/dist/chunk-JF245QUY.cjs +9 -0
- package/dist/{chunk-NNWJZUQL.cjs → chunk-OXCLLFCY.cjs} +2 -2
- package/dist/chunk-QBHOJGZI.cjs +21 -0
- package/dist/chunk-UQWQPTHO.js +3 -0
- package/dist/chunk-XOCFEVP4.js +6 -0
- package/dist/chunk-YIYGBTAM.js +9 -0
- package/dist/{client-C9Igm7O-.d.ts → client-CX8K7JNt.d.cts} +65 -11
- package/dist/{client-2KyVv-2l.d.cts → client-s-lEwqzc.d.ts} +65 -11
- package/dist/components.d.cts +43 -1
- package/dist/components.d.ts +43 -1
- package/dist/config.cjs +3 -3
- package/dist/config.d.cts +78 -74
- package/dist/config.d.ts +78 -74
- package/dist/config.js +2 -2
- package/dist/content.cjs +9 -9
- package/dist/content.d.cts +53 -9
- package/dist/content.d.ts +53 -9
- package/dist/content.js +2 -2
- package/dist/{errors-DpImb0Uf.d.cts → errors-Oax7djdp.d.ts} +54 -13
- package/dist/{errors-BGgJ--XR.d.ts → errors-welPvrD6.d.cts} +54 -13
- package/dist/index.cjs +12 -12
- package/dist/index.d.cts +27 -8
- package/dist/index.d.ts +27 -8
- package/dist/index.js +3 -3
- package/dist/react.cjs +4 -4
- package/dist/react.d.cts +24 -3
- package/dist/react.d.ts +24 -3
- package/dist/react.js +3 -3
- package/package.json +1 -1
- package/dist/chunk-5TUJX6GY.js +0 -5
- package/dist/chunk-LY73ZYN5.cjs +0 -20
- package/dist/chunk-NZWYOV5Y.cjs +0 -19
- package/dist/chunk-PYVUDQSQ.js +0 -3
- package/dist/chunk-YSKWSL2F.js +0 -9
- 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]`).
|
|
236
|
-
|
|
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
|
|
4
|
-
require('./chunk-
|
|
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(
|
|
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-
|
|
2
|
-
import './chunk-
|
|
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
|
-
|
|
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 };
|
|
@@ -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<"
|
|
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<"
|
|
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<
|
|
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<
|
|
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<"
|
|
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<"
|
|
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<
|
|
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<
|
|
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;
|
package/dist/checkout.d.cts
CHANGED
|
@@ -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 };
|
package/dist/checkout.d.ts
CHANGED
|
@@ -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 };
|