@stacksjs/actions 0.75.34 → 0.75.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/blog.d.ts +9 -8
- package/dist/blog.js +2 -2
- package/dist/dev/views.js +1 -1
- package/dist/seo.d.ts +129 -0
- package/dist/seo.js +4 -0
- package/dist/site-origin.d.ts +16 -0
- package/dist/site-origin.js +1 -0
- package/package.json +41 -41
package/dist/blog.d.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
+
import { usableOrigin } from './site-origin';
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
+
* The published blog's paths, for the site-wide `/sitemap.xml` to merge in
|
|
4
|
+
* (see `./seo.ts`). Empty when there are no published posts, so an app with an
|
|
5
|
+
* empty `content/blog` does not advertise a listing page.
|
|
3
6
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* literal ciphertext there, and prefixing it with `https://` produced feed and
|
|
7
|
-
* sitemap URLs like `https://encrypted:tLq7…==/blog/introducing-stacks` on the
|
|
8
|
-
* live site. Anything that is not a parseable http(s) origin with a dotted
|
|
9
|
-
* hostname is rejected here so callers fall back to the configured site url.
|
|
7
|
+
* Driven by `listPosts()` like the feed and `/blog/sitemap.xml`, so a draft is
|
|
8
|
+
* missing from every one of them rather than from some.
|
|
10
9
|
*/
|
|
11
|
-
export declare function
|
|
10
|
+
export declare function blogSitemapPaths(contentDir?: string): string[];
|
|
12
11
|
/**
|
|
13
12
|
* Render a blog request via BunPress, or return null to let the caller handle
|
|
14
13
|
* non-blog paths. Handles the listing, posts, the RSS feed, and the sitemap.
|
|
@@ -28,3 +27,5 @@ export declare function renderBlogFeed(req: Request): Promise<Response | null>;
|
|
|
28
27
|
*/
|
|
29
28
|
export declare function buildBlog(options?: { outDir: string, baseUrl?: string }): Promise<void>;
|
|
30
29
|
declare type BlogThemeMode = 'colored' | 'light' | 'dark';
|
|
30
|
+
// Re-exported: it lived here first, and callers import it from this module.
|
|
31
|
+
export { usableOrigin };
|
package/dist/blog.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{existsSync,mkdirSync,readdirSync,readFileSync,writeFileSync}from"node:fs";import{homedir}from"node:os";import{join}from"node:path";import{articleCss,articleScript,renderPostHtml}from"@stacksjs/cms/article";const CONTENT_DIR=join(process.cwd(),"content/blog"),THEME_FILE=join(CONTENT_DIR,".theme.css");function blogDefaults(){const appName=(process.env.APP_NAME||"").trim(),appUrl=(process.env.APP_URL||"").trim().replace(/\/+$/,""),title=appName?`${appName} Blog`:"Blog";return{title,description:"",siteTitle:title,author:appName,url:appUrl&&!/^https?:\/\//.test(appUrl)?`https://${appUrl}`:appUrl,nav:[{text:"Home",link:"/"},{text:"Blog",link:"/blog"}],themes:["colored","light","dark"],defaultTheme:"colored",colophon:'<a href="/blog/feed.xml">RSS</a>',emptyTitle:"No posts yet",emptyText:"The first one is in the works. Leave your email and it will land in your inbox the moment it ships."}}let sitePromise=null;function site(){if(!sitePromise)sitePromise=(async()=>{try{const cfg=(await import(join(process.cwd(),"config/blog.ts"))).default??{},merged={...blogDefaults(),...cfg};if(!cfg.siteTitle&&cfg.title)merged.siteTitle=cfg.title;return merged}catch{return blogDefaults()}})();return sitePromise}let bunpressPromise=null;async function loadBunPress(){if(!bunpressPromise)bunpressPromise=(async()=>{const candidates=[join(homedir(),"Code/Tools/bunpress/packages/bunpress/dist/src/index.js"),join(process.cwd(),"pantry/@stacksjs/bunpress/dist/src/index.js")].filter((entry)=>existsSync(entry));try{candidates.push(Bun.resolveSync("@stacksjs/bunpress",import.meta.dir))}catch{}for(const entry of new Set(candidates))try{return await import(entry)}catch(cause){const detail=cause instanceof Error?cause.message:String(cause);throw Error(`[blog] Failed to import BunPress from ${entry}: ${detail}`,{cause})}return null})();return bunpressPromise}function escapeHtml(s){return String(s).replace(/[&<>"]/g,(c)=>({"&":"&","<":"<",">":">",'"':"""})[c])}function escapeXml(s){return String(s).replace(/[&<>"']/g,(c)=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[c])}
|
|
1
|
+
import{existsSync,mkdirSync,readdirSync,readFileSync,writeFileSync}from"node:fs";import{homedir}from"node:os";import{join}from"node:path";import{articleCss,articleScript,renderPostHtml}from"@stacksjs/cms/article";import{usableOrigin}from"./site-origin";export{usableOrigin};const CONTENT_DIR=join(process.cwd(),"content/blog"),THEME_FILE=join(CONTENT_DIR,".theme.css");function blogDefaults(){const appName=(process.env.APP_NAME||"").trim(),appUrl=(process.env.APP_URL||"").trim().replace(/\/+$/,""),title=appName?`${appName} Blog`:"Blog";return{title,description:"",siteTitle:title,author:appName,url:appUrl&&!/^https?:\/\//.test(appUrl)?`https://${appUrl}`:appUrl,nav:[{text:"Home",link:"/"},{text:"Blog",link:"/blog"}],themes:["colored","light","dark"],defaultTheme:"colored",colophon:'<a href="/blog/feed.xml">RSS</a>',emptyTitle:"No posts yet",emptyText:"The first one is in the works. Leave your email and it will land in your inbox the moment it ships."}}let sitePromise=null;function site(){if(!sitePromise)sitePromise=(async()=>{try{const cfg=(await import(join(process.cwd(),"config/blog.ts"))).default??{},merged={...blogDefaults(),...cfg};if(!cfg.siteTitle&&cfg.title)merged.siteTitle=cfg.title;return merged}catch{return blogDefaults()}})();return sitePromise}let bunpressPromise=null;async function loadBunPress(){if(!bunpressPromise)bunpressPromise=(async()=>{const candidates=[join(homedir(),"Code/Tools/bunpress/packages/bunpress/dist/src/index.js"),join(process.cwd(),"pantry/@stacksjs/bunpress/dist/src/index.js")].filter((entry)=>existsSync(entry));try{candidates.push(Bun.resolveSync("@stacksjs/bunpress",import.meta.dir))}catch{}for(const entry of new Set(candidates))try{return await import(entry)}catch(cause){const detail=cause instanceof Error?cause.message:String(cause);throw Error(`[blog] Failed to import BunPress from ${entry}: ${detail}`,{cause})}return null})();return bunpressPromise}function escapeHtml(s){return String(s).replace(/[&<>"]/g,(c)=>({"&":"&","<":"<",">":">",'"':"""})[c])}function escapeXml(s){return String(s).replace(/[&<>"']/g,(c)=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[c])}function postUrl(baseUrl,slug){return`${baseUrl.replace(/\/$/,"")}/blog/${slug}`}function rssDate(date){if(!date)return"";const parsed=new Date(date);return Number.isNaN(parsed.getTime())?date:parsed.toUTCString()}async function fallbackFeedXml(baseUrl){const cfg=await site(),origin=usableOrigin(baseUrl)||usableOrigin(cfg.url)||cfg.url,items=listPosts().slice(0,50).map((p)=>{const url=postUrl(origin,p.slug);return` <item>
|
|
2
2
|
<title>${escapeXml(p.fm.title||p.slug)}</title>
|
|
3
3
|
<link>${escapeXml(url)}</link>
|
|
4
4
|
<guid>${escapeXml(url)}</guid>
|
|
@@ -69,7 +69,7 @@ ${[`${origin}/blog`,...listPosts().map((p)=>postUrl(origin,p.slug))].map((url)=>
|
|
|
69
69
|
<p class="colophon">${(await site()).colophon}</p>
|
|
70
70
|
</footer>`}async function blogConfig(bp,fm){const cfg=await site(),themeCss=existsSync(THEME_FILE)?readFileSync(THEME_FILE,"utf-8"):"",baseCss=bp.defaultConfig.markdown?.css||"";return{...bp.defaultConfig,title:fm.title||cfg.title,description:fm.description||cfg.description,docsDir:CONTENT_DIR,theme:"bun",markdown:{...bp.defaultConfig.markdown,css:`${baseCss}
|
|
71
71
|
${articleCss()}
|
|
72
|
-
${themeCss}`},themeConfig:{siteTitle:cfg.siteTitle,nav:cfg.nav}}}function listPosts(){if(!existsSync(
|
|
72
|
+
${themeCss}`},themeConfig:{siteTitle:cfg.siteTitle,nav:cfg.nav}}}export function blogSitemapPaths(contentDir=CONTENT_DIR){const posts=listPosts(contentDir);if(posts.length===0)return[];return["/blog",...posts.map((post)=>`/blog/${post.slug}`)]}function listPosts(contentDir=CONTENT_DIR){if(!existsSync(contentDir))return[];return readdirSync(contentDir).filter((f)=>f.endsWith(".md")).map((f)=>{const{data}=parseFrontmatter(readFileSync(join(contentDir,f),"utf-8"));return{slug:f.replace(/\.md$/,""),fm:data}}).filter((p)=>!!p.fm.title&&!!p.fm.date&&p.fm.draft!=="true").sort((a,b)=>{const fa=a.fm.featured==="true"?1:0,fb=b.fm.featured==="true"?1:0;if(fa!==fb)return fb-fa;return(b.fm.date||"").localeCompare(a.fm.date||"")})}async function notFoundHtml(bp){const body=`
|
|
73
73
|
<a class="blog-back" href="/blog">\u2190 All posts</a>
|
|
74
74
|
<div class="blog-404">
|
|
75
75
|
<p class="blog-kicker">404</p>
|
package/dist/dev/views.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{existsSync}from"node:fs";import{join}from"node:path";import{config,enterRequestScope,feature,installRequestScope,overridesReady,resolveViewPatterns}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{projectPath,siteConfigPath}from"@stacksjs/path";import{seedCsrfPageResponse}from"./csrf";import{resolveDefaultsResources}from"./defaults-resources";import{exitWithParent}from"./exit-with-parent";installRequestScope();exitWithParent();const projectServe=projectPath("serve.ts");try{if(await Bun.file(projectServe).exists())await import(projectServe);else await startDefaultServer()}catch{await startDefaultServer()}async function startDefaultServer(){await overridesReady;const{applyViewSecurityHeaders,describeApiProxyRules,describeRedirectRules,describeRewriteRules,injectGlobalAutoImports,isApiBoundRequest,proxyToBackend,resolveApiProxyRules,resolveEmbeddableRules,resolveRedirect,resolveRedirectRules,resolveRewrite,resolveRewriteRules}=await import("@stacksjs/server"),{applyRequestLocale}=await import("@stacksjs/i18n");await injectGlobalAutoImports();const{serve}=await import("bun-plugin-stx/serve"),{site:siteConfig,i18n:i18nConfig}=await loadStxSiteConfig(),defaultsResources=resolveDefaultsResources(),userViewsPath="resources/views",defaultViewsPath=join(defaultsResources,"views"),userComponentsPath=await firstExistingPath(["resources/components","components"])??"resources/components",userLayoutsPath=await firstExistingPath(["resources/views/layouts","resources/layouts"])??"resources/views/layouts",defaultLayoutsPath=join(defaultsResources,"layouts"),userPartialsPath=await firstExistingPath(["resources/partials","resources/views/partials","partials","resources/components"]),preferredPort=Number(process.env.PORT)||3000,apiPort=Number(process.env.PORT_API)||3008,docsPort=Number(process.env.PORT_DOCS)||config.ports?.docs||3006,apiBase=`http://127.0.0.1:${apiPort}`,docsBase=`http://127.0.0.1:${docsPort}`,apiProxyRules=resolveApiProxyRules(config.server?.proxy),redirectRules=resolveRedirectRules(config.server?.redirects),rewriteRules=resolveRewriteRules(config.server?.rewrites),embeddableRules=resolveEmbeddableRules(config.server?.security?.embeddable);if(apiProxyRules.paths.length>0||apiProxyRules.prefixes.length>1)console.log(` API proxy: ${describeApiProxyRules(apiProxyRules)}`);if(embeddableRules.paths.length>0||embeddableRules.prefixes.length>0)console.log(` Frameable by other origins: ${[...embeddableRules.paths,...embeddableRules.prefixes].join(" ")}`);if(redirectRules.size>0)
|
|
1
|
+
import{existsSync}from"node:fs";import{join}from"node:path";import{config,enterRequestScope,feature,installRequestScope,overridesReady,resolveViewPatterns}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{projectPath,siteConfigPath}from"@stacksjs/path";import{seedCsrfPageResponse}from"./csrf";import{resolveDefaultsResources}from"./defaults-resources";import{exitWithParent}from"./exit-with-parent";installRequestScope();exitWithParent();const projectServe=projectPath("serve.ts");try{if(await Bun.file(projectServe).exists())await import(projectServe);else await startDefaultServer()}catch{await startDefaultServer()}async function startDefaultServer(){await overridesReady;const{applyViewSecurityHeaders,describeApiProxyRules,describeRedirectRules,describeRewriteRules,injectGlobalAutoImports,isApiBoundRequest,proxyToBackend,resolveApiProxyRules,resolveEmbeddableRules,resolveRedirect,resolveRedirectRules,resolveRewrite,resolveRewriteRules}=await import("@stacksjs/server"),{applyRequestLocale}=await import("@stacksjs/i18n");await injectGlobalAutoImports();const{serve}=await import("bun-plugin-stx/serve"),{site:siteConfig,i18n:i18nConfig}=await loadStxSiteConfig(),defaultsResources=resolveDefaultsResources(),userViewsPath="resources/views",defaultViewsPath=join(defaultsResources,"views"),userComponentsPath=await firstExistingPath(["resources/components","components"])??"resources/components",userLayoutsPath=await firstExistingPath(["resources/views/layouts","resources/layouts"])??"resources/views/layouts",defaultLayoutsPath=join(defaultsResources,"layouts"),userPartialsPath=await firstExistingPath(["resources/partials","resources/views/partials","partials","resources/components"]),preferredPort=Number(process.env.PORT)||3000,apiPort=Number(process.env.PORT_API)||3008,docsPort=Number(process.env.PORT_DOCS)||config.ports?.docs||3006,apiBase=`http://127.0.0.1:${apiPort}`,docsBase=`http://127.0.0.1:${docsPort}`,apiProxyRules=resolveApiProxyRules(config.server?.proxy),redirectRules=resolveRedirectRules(config.server?.redirects),rewriteRules=resolveRewriteRules(config.server?.rewrites),embeddableRules=resolveEmbeddableRules(config.server?.security?.embeddable);if(apiProxyRules.paths.length>0||apiProxyRules.prefixes.length>1)console.log(` API proxy: ${describeApiProxyRules(apiProxyRules)}`);if(embeddableRules.paths.length>0||embeddableRules.prefixes.length>0)console.log(` Frameable by other origins: ${[...embeddableRules.paths,...embeddableRules.prefixes].join(" ")}`);if(redirectRules.size>0)console.log(` Redirects: ${describeRedirectRules(redirectRules)}`);if(rewriteRules.size>0)console.log(` Rewrites: ${describeRewriteRules(rewriteRules)}`);const{createSiteSeoHandler,describeSeoAtBoot}=await import("../seo"),seoOptions={root:projectPath(),seo:config.app?.seo,appUrl:config.app?.url},siteSeo=createSiteSeoHandler({...seoOptions,cacheTtlMs:0});for(const note of await describeSeoAtBoot(seoOptions).catch(()=>[]))log.warn(note);const hasUserDocsView=["docs.stx","docs/index.stx"].some((rel)=>existsSync(projectPath(`${userViewsPath}/${rel}`))),hasDocsSite=existsSync(projectPath("docs")),docsProxyEnabled=!hasUserDocsView&&hasDocsSite,{authCookieName,stxPageAuthMiddleware}=await import("@stacksjs/auth"),{enhanceRequest,loadMiddlewareHandlers}=await import("@stacksjs/router"),authCookie=authCookieName(),pageMiddleware=await loadMiddlewareHandlers(),{mountedDefaultRouteBundles}=await import("@stacksjs/router"),viewPatterns=resolveViewPatterns(userViewsPath,defaultViewsPath,config?.ui?.defaultViews,(path)=>existsSync(projectPath(path)),void 0,mountedDefaultRouteBundles(feature));for(const name of viewPatterns.missing)log.warn(`ui.defaultViews lists "${name}", which does not exist under ${defaultViewsPath} - ignoring.`);if(viewPatterns.withheld.length>0)log.debug(`Default views without a mounted route bundle behind them, not served: ${viewPatterns.withheld.join(", ")}`);await serve({patterns:viewPatterns.patterns,exclude:viewPatterns.exclude,port:preferredPort,componentsDir:userComponentsPath,fallbackComponentsDir:join(defaultsResources,"components"),layoutsDir:userLayoutsPath,...userPartialsPath&&{partialsDir:userPartialsPath},fallbackLayoutsDir:defaultLayoutsPath,fallbackPartialsDir:defaultViewsPath,quiet:!0,openPath:process.env.STACKS_DEV_ENTRY_PATH||"/",...i18nConfig&&{i18n:i18nConfig},...siteConfig?.url&&{site:siteConfig},auth:{cookieName:authCookie,redirectTo:"/login"},middleware:{...pageMiddleware,...stxPageAuthMiddleware({cookieName:authCookie,redirectTo:"/login"})},prepareMiddlewareRequest:(request)=>enhanceRequest(request),onRequest:async(req)=>{const snapshot=enterRequestScope(req),url=new URL(req.url),{maintenanceGate}=await import("@stacksjs/server"),gated=await maintenanceGate(req);if(gated)return gated;const redirected=resolveRedirect(url,redirectRules);if(redirected)return redirected;const rewritten=resolveRewrite(url.pathname,rewriteRules);if(rewritten){const target=new URL(`${rewritten}${url.search}`,url.origin);return proxyToBackend(new Request(target,req),apiBase)}const seoFile=await siteSeo(req);if(seoFile)return seoFile;if(existsSync(projectPath("resources/views/blog.stx"))){const{renderBlogFeed}=await import("../blog"),feed=await renderBlogFeed(req);if(feed)return feed}else{const{renderBlog}=await import("../blog"),blogResponse=await renderBlog(req);if(blogResponse)return blogResponse}if(docsProxyEnabled&&(url.pathname==="/docs"||url.pathname.startsWith("/docs/")))return proxyToBackend(req,docsBase,"/docs");if(isApiBoundRequest(req,url.pathname,apiProxyRules))return proxyToBackend(req,apiBase);if(i18nConfig&&req.method==="GET"){const localeSwitch=url.pathname.match(/^\/locale\/([a-z]{2}(?:-[a-z]{2})?)\/?$/i);if(localeSwitch){const{createLocaleSwitchResponse}=await import("@stacksjs/i18n");return createLocaleSwitchResponse(req,localeSwitch[1],i18nConfig)}}snapshot.locale=await applyRequestLocale(req);if(config.sites?.enabled){const sites=await import("@stacksjs/sites"),resolved=await sites.resolveSiteByHost(sites.requestHost(req.headers,sites.sitesOptions()));sites.setCurrentSite(resolved);snapshot.site=sites.toSiteSnapshot(resolved)}if(config.analytics?.capturePageviews)import("@stacksjs/analytics").then(({recordPageview})=>recordPageview(req)).catch(()=>{});return null},onResponse:async(req,response)=>{const secured=applyViewSecurityHeaders(req,response,embeddableRules),baseline=secured??response;let current=baseline;if(current.status===404&&config.sites?.enabled)try{const{cmsNotFoundFallback}=await import("@stacksjs/cms"),cmsResponse=await cmsNotFoundFallback(req);if(cmsResponse)current=cmsResponse}catch(error){log.debug(`CMS fallback skipped: ${error.message}`)}return await seedCsrfPageResponse(req,current)??(current===baseline?secured:current)}})}async function firstExistingPath(candidates){const existing=candidates.filter((candidate)=>existsSync(projectPath(candidate)));if(existing.length===0)return null;for(const candidate of existing)if(containsTemplates(projectPath(candidate)))return candidate;return existing[0]}function containsTemplates(dir){try{return[...new Bun.Glob("**/*.stx").scanSync({cwd:dir,onlyFiles:!0})].length>0}catch{return!1}}function fallbackI18nFromSite(site){const locales=site.i18n.locales,defaultLocale=site.i18n.defaultLocale??locales[0];return{locales,defaultLocale,labels:site.i18n.labels??Object.fromEntries(locales.map((c)=>[c,c.toUpperCase()])),translations:{},pickerSelector:site.i18n.pickerSelector??"#lang-picker"}}async function resolveSiteI18n(site){try{const{resolveI18n}=await import("@stacksjs/stx");if(typeof resolveI18n==="function"){const i18n=resolveI18n(site,projectPath());if(i18n)return i18n}}catch{}return fallbackI18nFromSite(site)}async function loadStxSiteConfig(){const sitePath=siteConfigPath();if(!existsSync(sitePath))return{};try{const mod=await import(sitePath),site=mod.default??mod.site??mod.config;if(!site)return{};if(!site.i18n)return{site};const i18n=await resolveSiteI18n(site);return{site,i18n}}catch{}return{}}
|
package/dist/seo.d.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { SeoOptions, SeoRobotsRule, SeoSitemapEntry } from '@stacksjs/types';
|
|
2
|
+
/**
|
|
3
|
+
* Whether a view's source says it must stay out of search.
|
|
4
|
+
*
|
|
5
|
+
* `noindex` is the one signal: a page that tells crawlers not to index it is
|
|
6
|
+
* never listed, because a sitemap entry for a `noindex` page is a
|
|
7
|
+
* contradiction search consoles report as an error. It is recognised in the
|
|
8
|
+
* three spellings a Stacks page uses:
|
|
9
|
+
*
|
|
10
|
+
* 1. `<meta name="robots" content="noindex">`, written in the page.
|
|
11
|
+
* 2. `robots: 'noindex'` in a head/SEO object (`useHead`, `useSeoMeta`,
|
|
12
|
+
* `definePageMeta`).
|
|
13
|
+
* 3. `const noindex = true` in the page's server script - the flag a shared
|
|
14
|
+
* head partial turns into the tag, which is how a page that includes its
|
|
15
|
+
* `<head>` from a partial says it. A static scan cannot follow the
|
|
16
|
+
* include, so the flag has to be spelled with that name to be seen.
|
|
17
|
+
*
|
|
18
|
+
* `definePageMeta({ sitemap: false })` is also honoured, for a page that may
|
|
19
|
+
* be indexed but should not be advertised; it is stx's static-build spelling.
|
|
20
|
+
*/
|
|
21
|
+
export declare function declaresNoindex(source: string): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Whether a view is gated by the `auth` page middleware
|
|
24
|
+
* (`definePageMeta({ middleware: ['auth'] })`). A crawler following its
|
|
25
|
+
* sitemap entry is redirected to `/login`, so listing it only advertises the
|
|
26
|
+
* login page under another name.
|
|
27
|
+
*/
|
|
28
|
+
export declare function requiresAuth(source: string): boolean;
|
|
29
|
+
/** Error and holding pages: rendered on a status, never navigated to. */
|
|
30
|
+
export declare function isErrorViewPath(path: string): boolean;
|
|
31
|
+
/** True for a path inside a non-page directory, or a `_`/`.`-prefixed segment. */
|
|
32
|
+
export declare function isNonPagePath(path: string): boolean;
|
|
33
|
+
/** `/a/[slug]`, `/[[lang]]`, `/[...rest]` - anything stx fills from the URL. */
|
|
34
|
+
export declare function isDynamicPath(path: string): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Whether `pattern` from config matches `path`: an entry ending in `/` or `*`
|
|
37
|
+
* matches everything under it (and the bare path itself), anything else is an
|
|
38
|
+
* exact path.
|
|
39
|
+
*/
|
|
40
|
+
export declare function pathMatchesExclude(path: string, pattern: string): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Whether robots.txt's `*` group blocks `path`: the longest matching rule
|
|
43
|
+
* wins, and an `Allow` wins a tie (RFC 9309 §2.2.2).
|
|
44
|
+
*/
|
|
45
|
+
export declare function isDisallowedByRobots(path: string, group: { allow?: readonly string[], disallow?: readonly string[] }): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* The `*` group robots.txt publishes: `Allow: /`, the framework's defaults and
|
|
48
|
+
* the app's own `disallow`, deduplicated in that order.
|
|
49
|
+
*/
|
|
50
|
+
export declare function robotsDefaultGroup(seo?: SeoOptions): SeoRobotsRule;
|
|
51
|
+
/**
|
|
52
|
+
* The origin every generated URL is built on: `seo.origin`, then `app.url`
|
|
53
|
+
* (APP_URL), then the request's own origin - the last only for local
|
|
54
|
+
* development, where APP_URL is often a bare `localhost`.
|
|
55
|
+
*/
|
|
56
|
+
export declare function resolveSeoOrigin(options: { seo?: SeoOptions, appUrl?: string, request?: Request }): string;
|
|
57
|
+
/** robots.txt for the app, pointing at the sitemap when there is one. */
|
|
58
|
+
export declare function buildRobotsTxt(options: { seo?: SeoOptions, origin: string }): string;
|
|
59
|
+
/** The path part of a sitemap `loc`, or the loc itself when it is a path. */
|
|
60
|
+
export declare function locPath(loc: string): string;
|
|
61
|
+
/** Every `<loc>` in a sitemap or sitemap index. */
|
|
62
|
+
export declare function sitemapLocs(xml: string): string[];
|
|
63
|
+
/**
|
|
64
|
+
* The app's own static pages, with the reason each other view was skipped.
|
|
65
|
+
*
|
|
66
|
+
* Discovery and `lastmod` come from stx's `scanForSitemapEntries`; everything
|
|
67
|
+
* that decides whether a view is a public, indexable page is applied here, on
|
|
68
|
+
* the path it reports and the source of the view behind it.
|
|
69
|
+
*/
|
|
70
|
+
export declare function scanViewSitemapEntries(viewsDir: string, seo?: SeoOptions): Promise<ViewSitemapScan>;
|
|
71
|
+
/**
|
|
72
|
+
* Everything `/sitemap.xml` lists: the views, the blog, and config, with
|
|
73
|
+
* config's `exclude` and robots.txt's `Disallow` applied to all of them and
|
|
74
|
+
* each path listed once.
|
|
75
|
+
*/
|
|
76
|
+
export declare function collectSitemapEntries(inputs: SitemapInputs): Promise<SeoSitemapEntry[]>;
|
|
77
|
+
/** `/sitemap.xml` for the app. */
|
|
78
|
+
export declare function buildSitemapXml(inputs: SitemapInputs): Promise<string>;
|
|
79
|
+
/**
|
|
80
|
+
* Paths the generated sitemap would list that a hand-written one does not, for
|
|
81
|
+
* the boot warning. Empty when the hand-written file is complete.
|
|
82
|
+
*/
|
|
83
|
+
export declare function pathsMissingFromSitemap(handwrittenXml: string, generated: readonly SeoSitemapEntry[]): string[];
|
|
84
|
+
/** Which generated file a request is for, or undefined. */
|
|
85
|
+
export declare function seoFileFor(req: Request): SeoFile | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* The views server's handler: a Response for `/sitemap.xml` and `/robots.txt`,
|
|
88
|
+
* or null for every other request and whenever a hand-written file in
|
|
89
|
+
* `public/` should be served instead (the static handler after `onRequest`
|
|
90
|
+
* then serves it).
|
|
91
|
+
*/
|
|
92
|
+
export declare function createSiteSeoHandler(options?: SiteSeoHandlerOptions): (req: Request) => Promise<Response | null>;
|
|
93
|
+
/**
|
|
94
|
+
* Warnings for the views server to print at boot: a hand-written file that
|
|
95
|
+
* shadows the generated one and has fallen behind the views.
|
|
96
|
+
*
|
|
97
|
+
* Only the sitemap can be checked for staleness. A hand-written robots.txt is
|
|
98
|
+
* reported only when it names no sitemap at all, since its rules are the
|
|
99
|
+
* app's to choose.
|
|
100
|
+
*/
|
|
101
|
+
export declare function describeSeoAtBoot(options?: SiteSeoHandlerOptions): Promise<string[]>;
|
|
102
|
+
/**
|
|
103
|
+
* Directories under a page root whose contents are never pages. The same list
|
|
104
|
+
* as `NON_PAGE_DIRS` in stx-router's `page-routes.ts`, which decides what stx
|
|
105
|
+
* routes - repeated here because stx-router is not a dependency of this
|
|
106
|
+
* package, and a page stx refuses to route must not be advertised.
|
|
107
|
+
*/
|
|
108
|
+
export declare const NON_PAGE_DIRS: readonly string[];
|
|
109
|
+
/** Always disallowed for every crawler: JSON endpoints are not pages. */
|
|
110
|
+
export declare const DEFAULT_ROBOTS_DISALLOW: readonly string[];
|
|
111
|
+
export declare interface ViewSitemapScan {
|
|
112
|
+
entries: SeoSitemapEntry[]
|
|
113
|
+
skipped: { path: string, reason: SitemapSkipReason }[]
|
|
114
|
+
}
|
|
115
|
+
export declare interface SitemapInputs {
|
|
116
|
+
root: string
|
|
117
|
+
seo?: SeoOptions
|
|
118
|
+
origin: string
|
|
119
|
+
skipDynamicEntries?: boolean
|
|
120
|
+
}
|
|
121
|
+
export declare interface SiteSeoHandlerOptions {
|
|
122
|
+
root?: string
|
|
123
|
+
seo?: SeoOptions
|
|
124
|
+
appUrl?: string
|
|
125
|
+
cacheTtlMs?: number
|
|
126
|
+
}
|
|
127
|
+
/** Why a view was left out of the sitemap, for the boot report and tests. */
|
|
128
|
+
export type SitemapSkipReason = 'dynamic' | 'error-page' | 'non-page' | 'noindex' | 'auth' | 'excluded' | 'disallowed';
|
|
129
|
+
declare type SeoFile = 'sitemap.xml' | 'robots.txt';
|
package/dist/seo.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{existsSync,readFileSync,statSync}from"node:fs";import{join}from"node:path";import{generateRobotsTxt,generateSitemap,scanForSitemapEntries}from"@stacksjs/stx/seo";import{usableOrigin}from"./site-origin";export const NON_PAGE_DIRS=["components","layouts","partials","emails"],DEFAULT_ROBOTS_DISALLOW=["/api/"];function withoutComments(source){return source.replace(/<!--[\s\S]*?-->/g,"").replace(/\{\{--[\s\S]*?--\}\}/g,"").replace(/\/\*[\s\S]*?\*\//g,"").replace(/(^|[^:\\'"`])\/\/[^\n]*/g,"$1")}export function declaresNoindex(source){const code=withoutComments(source);for(const tag of code.match(/<meta\b[^>]*>/gi)??[])if(/\bname\s*=\s*["']?(?:robots|googlebot)\b/i.test(tag)&&/\bcontent\s*=\s*["'][^"']*\bnoindex\b/i.test(tag))return!0;if(/\brobots\s*:\s*(["'`])[^"'`]*\bnoindex\b/i.test(code))return!0;if(/\b(?:const|let|var)\s+noindex\s*(?::\s*boolean\s*)?=\s*true\b/.test(code))return!0;if(/\bsitemap\s*:\s*false\b/.test(code))return!0;return!1}export function requiresAuth(source){const match=withoutComments(source).match(/\bmiddleware\s*:\s*(\[[^\]]*\]|["'][^"']*["'])/);return!!match&&/["']auth["']/.test(match[1])}export function isErrorViewPath(path){const segments=path.split("/").filter(Boolean),last=segments.at(-1)??"";return segments.includes("errors")||/^(?:\d{3}|error|coming-soon|maintenance)$/.test(last)}export function isNonPagePath(path){const segments=path.split("/").filter(Boolean);return segments.slice(0,-1).some((segment)=>NON_PAGE_DIRS.includes(segment))||segments.some((segment)=>segment.startsWith("_")||segment.startsWith("."))}export function isDynamicPath(path){return/\[[^\]]*\]/.test(path)}export function pathMatchesExclude(path,pattern){if(!pattern)return!1;if(pattern.endsWith("*")){const prefix=pattern.slice(0,-1);return path.startsWith(prefix)||`${path}/`===prefix}if(pattern.endsWith("/")&&pattern!=="/")return path.startsWith(pattern)||`${path}/`===pattern;return path===pattern||pattern!=="/"&&path===pattern.replace(/\/+$/,"")}function longestRobotsMatch(path,rules){let best=-1;for(const rule of rules){if(!rule)continue;const anchored=rule.endsWith("$"),pattern=(anchored?rule.slice(0,-1):rule).split("*").map((part)=>part.replace(/[.+?^${}()|[\]\\]/g,"\\$&")).join(".*");if(new RegExp(`^${pattern}${anchored?"$":""}`).test(path))best=Math.max(best,rule.length)}return best}export function isDisallowedByRobots(path,group){const disallow=longestRobotsMatch(path,group.disallow??[]);return disallow>=0&&disallow>longestRobotsMatch(path,group.allow??[])}function sitemapOptions(seo){if(seo?.sitemap===!1)return;return seo?.sitemap??{}}function robotsOptions(seo){if(seo?.robots===!1)return;return seo?.robots??{}}export function robotsDefaultGroup(seo){const disallow=[...new Set([...DEFAULT_ROBOTS_DISALLOW,...robotsOptions(seo)?.disallow??[]])];return{userAgent:"*",allow:["/"],disallow}}export function resolveSeoOrigin(options){const configured=usableOrigin(options.seo?.origin)||usableOrigin(options.appUrl);if(configured)return configured;if(!options.request)return"";const url=new URL(options.request.url),host=options.request.headers.get("x-forwarded-host")?.split(",")[0]?.trim()||url.host;return`${options.request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim()||url.protocol.replace(":","")}://${host}`}export function buildRobotsTxt(options){const robots=robotsOptions(options.seo),rules=[robotsDefaultGroup(options.seo),...robots?.rules??[]];return`# Generated by Stacks from resources/views and config/app.ts (seo).
|
|
2
|
+
${generateRobotsTxt({rules,...sitemapOptions(options.seo)&&options.origin&&{sitemap:`${options.origin}/sitemap.xml`}})}
|
|
3
|
+
`}function toEntry(value){return typeof value==="string"?{loc:value}:value}export function locPath(loc){if(!/^https?:\/\//i.test(loc))return loc.startsWith("/")?loc:`/${loc}`;try{return new URL(loc).pathname}catch{return loc}}export function sitemapLocs(xml){return[...xml.matchAll(/<loc>\s*([^<]+?)\s*<\/loc>/gi)].map((match)=>match[1].replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"').replace(/'/g,"'"))}function viewFileFor(viewsDir,path){const rel=path==="/"?"index":path.replace(/^\//,"");for(const candidate of[`${rel}.stx`,`${rel}/index.stx`]){const file=join(viewsDir,candidate);if(existsSync(file))return file}return}export async function scanViewSitemapEntries(viewsDir,seo){const result={entries:[],skipped:[]};if(!existsSync(viewsDir))return result;const scanned=await scanForSitemapEntries(viewsDir,{baseUrl:"",extensions:[".stx"],ignore:["node_modules"]}),exclude=sitemapOptions(seo)?.exclude??[],robotsGroup=robotsOptions(seo)?robotsDefaultGroup(seo):void 0,seen=new Set;for(const entry of scanned){const path=entry.loc;if(seen.has(path))continue;seen.add(path);const skip=(reason)=>{result.skipped.push({path,reason})};if(isNonPagePath(path)){skip("non-page");continue}if(isDynamicPath(path)){skip("dynamic");continue}if(isErrorViewPath(path)){skip("error-page");continue}if(exclude.some((pattern)=>pathMatchesExclude(path,pattern))){skip("excluded");continue}if(robotsGroup&&isDisallowedByRobots(path,robotsGroup)){skip("disallowed");continue}const file=viewFileFor(viewsDir,path),source=file?readFileSync(file,"utf8"):"";if(declaresNoindex(source)){skip("noindex");continue}if(requiresAuth(source)){skip("auth");continue}result.entries.push({loc:path,...entry.lastmod&&{lastmod:entry.lastmod},priority:path==="/"?1:0.5})}return result}async function blogEntries(root){const contentDir=join(root,"content/blog");if(existsSync(contentDir))try{const{blogSitemapPaths}=await import("./blog"),paths=blogSitemapPaths(contentDir);if(paths.length>0)return paths.map((loc)=>({loc,priority:loc==="/blog"?0.7:0.6}))}catch{}const built=join(root,"dist/blog/sitemap.xml");if(existsSync(built))try{return sitemapLocs(readFileSync(built,"utf8")).map((loc)=>({loc:locPath(loc),priority:0.6}))}catch{}return[]}export async function collectSitemapEntries(inputs){const options=sitemapOptions(inputs.seo);if(!options)return[];const views=await scanViewSitemapEntries(join(inputs.root,"resources/views"),inputs.seo),configured=[...options.include??[]];if(options.entries&&!inputs.skipDynamicEntries)try{configured.push(...await options.entries())}catch(error){console.warn(`[seo] seo.sitemap.entries() failed; serving the sitemap without them: ${error.message}`)}const exclude=options.exclude??[],robotsGroup=robotsOptions(inputs.seo)?robotsDefaultGroup(inputs.seo):void 0,admits=(path)=>!exclude.some((pattern)=>pathMatchesExclude(path,pattern))&&!(robotsGroup&&isDisallowedByRobots(path,robotsGroup)),byPath=new Map;for(const entry of views.entries)byPath.set(entry.loc,entry);for(const entry of await blogEntries(inputs.root)){const path=locPath(entry.loc);if(!byPath.has(path)&&admits(path))byPath.set(path,entry)}for(const raw of configured){const entry=toEntry(raw),path=locPath(entry.loc);if(admits(path))byPath.set(path,{...byPath.get(path),...entry})}return[...byPath.values()].sort((a,b)=>locPath(a.loc).localeCompare(locPath(b.loc)))}export async function buildSitemapXml(inputs){const entries=await collectSitemapEntries(inputs);return`${generateSitemap(entries,{baseUrl:inputs.origin,includeLastmod:!1})}
|
|
4
|
+
`}export function pathsMissingFromSitemap(handwrittenXml,generated){const listed=new Set(sitemapLocs(handwrittenXml).map((loc)=>locPath(loc).replace(/(.)\/+$/,"$1")));return generated.map((entry)=>locPath(entry.loc)).filter((path)=>!listed.has(path))}export function seoFileFor(req){if(req.method!=="GET"&&req.method!=="HEAD")return;const{pathname}=new URL(req.url);if(pathname==="/sitemap.xml")return"sitemap.xml";if(pathname==="/robots.txt")return"robots.txt";return}export function createSiteSeoHandler(options={}){const root=options.root??process.cwd(),ttl=options.cacheTtlMs??0,cache=new Map;return async(req)=>{const file=seoFileFor(req);if(!file)return null;const seo=options.seo;if(file==="sitemap.xml"&&!sitemapOptions(seo))return null;if(file==="robots.txt"&&!robotsOptions(seo))return null;if((seo?.handwritten??"prefer")==="prefer"&&existsSync(join(root,"public",file)))return null;const origin=resolveSeoOrigin({seo,appUrl:options.appUrl,request:req}),key=`${file} ${origin}`,cached=cache.get(key);let body=cached&&Date.now()-cached.at<ttl?cached.body:void 0;if(body===void 0){body=file==="sitemap.xml"?await buildSitemapXml({root,seo,origin}):buildRobotsTxt({seo,origin});if(ttl>0)cache.set(key,{at:Date.now(),body})}return new Response(req.method==="HEAD"?null:body,{headers:{"Content-Type":file==="sitemap.xml"?"application/xml; charset=utf-8":"text/plain; charset=utf-8","Cache-Control":ttl>0?"public, max-age=3600":"no-cache"}})}}export async function describeSeoAtBoot(options={}){const root=options.root??process.cwd(),seo=options.seo;if((seo?.handwritten??"prefer")!=="prefer")return[];const notes=[],sitemapFile=join(root,"public/sitemap.xml");if(sitemapOptions(seo)&&existsSync(sitemapFile)&&statSync(sitemapFile).isFile()){const generated=await collectSitemapEntries({root,seo,origin:"",skipDynamicEntries:!0}),missing=pathsMissingFromSitemap(readFileSync(sitemapFile,"utf8"),generated);if(missing.length>0)notes.push(`public/sitemap.xml is hand-written and does not list ${missing.length} page${missing.length===1?"":"s"} Stacks would: ${missing.slice(0,8).join(", ")}${missing.length>8?", ...":""}. Delete it to serve the generated /sitemap.xml, or set app.seo.handwritten to 'replace'.`)}const robotsFile=join(root,"public/robots.txt");if(robotsOptions(seo)&&existsSync(robotsFile)&&statSync(robotsFile).isFile()){if(!/^\s*sitemap\s*:/im.test(readFileSync(robotsFile,"utf8")))notes.push("public/robots.txt is hand-written and names no Sitemap. Delete it to serve the generated /robots.txt, which does.")}return notes}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one answer to "which origin goes into an absolute link", shared by the
|
|
3
|
+
* blog's feed and sitemap and the site-wide `/sitemap.xml` and `/robots.txt`
|
|
4
|
+
* (see ./seo.ts), so the two cannot disagree about what counts as usable.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* An origin safe to bake into absolute links, or '' when it is not usable.
|
|
8
|
+
*
|
|
9
|
+
* The deploy-time build takes its baseUrl from `APP_URL`, which is not always
|
|
10
|
+
* plaintext: a release that loads an env file it cannot decrypt leaves the
|
|
11
|
+
* literal ciphertext there, and prefixing it with `https://` produced feed and
|
|
12
|
+
* sitemap URLs like `https://encrypted:tLq7…==/blog/introducing-stacks` on the
|
|
13
|
+
* live site. Anything that is not a parseable http(s) origin with a dotted
|
|
14
|
+
* hostname is rejected here so callers fall back to the configured site url.
|
|
15
|
+
*/
|
|
16
|
+
export declare function usableOrigin(value?: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function usableOrigin(value){if(!value)return"";try{const url=new URL(/^https?:\/\//.test(value)?value:`https://${value}`);if(url.protocol!=="http:"&&url.protocol!=="https:")return"";if(!url.hostname.includes(".")||!/^[a-z0-9.-]+$/i.test(url.hostname))return"";return url.origin}catch{return""}}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/actions",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.36",
|
|
6
6
|
"description": "The Stacks actions.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -60,53 +60,53 @@
|
|
|
60
60
|
"prepublishOnly": "bun run build"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@stacksjs/action-runner": "0.75.
|
|
64
|
-
"@stacksjs/ai": "0.75.
|
|
65
|
-
"@stacksjs/analytics": "0.75.
|
|
66
|
-
"@stacksjs/api": "0.75.
|
|
67
|
-
"@stacksjs/auth": "0.75.
|
|
68
|
-
"@stacksjs/build": "0.75.
|
|
63
|
+
"@stacksjs/action-runner": "0.75.36",
|
|
64
|
+
"@stacksjs/ai": "0.75.36",
|
|
65
|
+
"@stacksjs/analytics": "0.75.36",
|
|
66
|
+
"@stacksjs/api": "0.75.36",
|
|
67
|
+
"@stacksjs/auth": "0.75.36",
|
|
68
|
+
"@stacksjs/build": "0.75.36",
|
|
69
69
|
"@stacksjs/bumpx": "^0.2.6",
|
|
70
70
|
"@stacksjs/bunpress": "^0.2.8",
|
|
71
|
-
"@stacksjs/cli": "0.75.
|
|
72
|
-
"@stacksjs/cloud": "0.75.
|
|
73
|
-
"@stacksjs/cms": "0.75.
|
|
74
|
-
"@stacksjs/components": "^0.2.
|
|
75
|
-
"@stacksjs/config": "0.75.
|
|
76
|
-
"@stacksjs/database": "0.75.
|
|
77
|
-
"@stacksjs/desktop-build": "0.75.
|
|
78
|
-
"@stacksjs/dns": "0.75.
|
|
79
|
-
"@stacksjs/enums": "0.75.
|
|
80
|
-
"@stacksjs/env": "0.75.
|
|
81
|
-
"@stacksjs/error-handling": "0.75.
|
|
71
|
+
"@stacksjs/cli": "0.75.36",
|
|
72
|
+
"@stacksjs/cloud": "0.75.36",
|
|
73
|
+
"@stacksjs/cms": "0.75.36",
|
|
74
|
+
"@stacksjs/components": "^0.2.343",
|
|
75
|
+
"@stacksjs/config": "0.75.36",
|
|
76
|
+
"@stacksjs/database": "0.75.36",
|
|
77
|
+
"@stacksjs/desktop-build": "0.75.36",
|
|
78
|
+
"@stacksjs/dns": "0.75.36",
|
|
79
|
+
"@stacksjs/enums": "0.75.36",
|
|
80
|
+
"@stacksjs/env": "0.75.36",
|
|
81
|
+
"@stacksjs/error-handling": "0.75.36",
|
|
82
82
|
"@stacksjs/gitit": "^0.2.5",
|
|
83
|
-
"@stacksjs/i18n": "0.75.
|
|
84
|
-
"@stacksjs/image": "0.75.
|
|
85
|
-
"@stacksjs/logging": "0.75.
|
|
83
|
+
"@stacksjs/i18n": "0.75.36",
|
|
84
|
+
"@stacksjs/image": "0.75.36",
|
|
85
|
+
"@stacksjs/logging": "0.75.36",
|
|
86
86
|
"@stacksjs/logsmith": "^0.2.3",
|
|
87
|
-
"@stacksjs/orm": "0.75.
|
|
88
|
-
"@stacksjs/path": "0.75.
|
|
89
|
-
"@stacksjs/payments": "0.75.
|
|
90
|
-
"@stacksjs/queue": "0.75.
|
|
91
|
-
"@stacksjs/registry": "0.75.
|
|
92
|
-
"@stacksjs/router": "0.75.
|
|
87
|
+
"@stacksjs/orm": "0.75.36",
|
|
88
|
+
"@stacksjs/path": "0.75.36",
|
|
89
|
+
"@stacksjs/payments": "0.75.36",
|
|
90
|
+
"@stacksjs/queue": "0.75.36",
|
|
91
|
+
"@stacksjs/registry": "0.75.36",
|
|
92
|
+
"@stacksjs/router": "0.75.36",
|
|
93
93
|
"@stacksjs/rpx": "^0.11.54",
|
|
94
|
-
"@stacksjs/scheduler": "0.75.
|
|
95
|
-
"@stacksjs/search-engine": "0.75.
|
|
96
|
-
"@stacksjs/security": "0.75.
|
|
97
|
-
"@stacksjs/server": "0.75.
|
|
98
|
-
"@stacksjs/sites": "0.75.
|
|
99
|
-
"@stacksjs/storage": "0.75.
|
|
100
|
-
"@stacksjs/strings": "0.75.
|
|
101
|
-
"@stacksjs/stx": "^0.2.
|
|
94
|
+
"@stacksjs/scheduler": "0.75.36",
|
|
95
|
+
"@stacksjs/search-engine": "0.75.36",
|
|
96
|
+
"@stacksjs/security": "0.75.36",
|
|
97
|
+
"@stacksjs/server": "0.75.36",
|
|
98
|
+
"@stacksjs/sites": "0.75.36",
|
|
99
|
+
"@stacksjs/storage": "0.75.36",
|
|
100
|
+
"@stacksjs/strings": "0.75.36",
|
|
101
|
+
"@stacksjs/stx": "^0.2.343",
|
|
102
102
|
"@stacksjs/tlsx": "^0.13.19",
|
|
103
103
|
"@stacksjs/ts-cloud": "^0.16.15",
|
|
104
104
|
"@stacksjs/ts-md": "^0.1.1",
|
|
105
|
-
"@stacksjs/tunnel": "0.75.
|
|
106
|
-
"@stacksjs/types": "0.75.
|
|
107
|
-
"@stacksjs/utils": "0.75.
|
|
108
|
-
"@stacksjs/whois": "0.75.
|
|
109
|
-
"bun-plugin-stx": "^0.2.
|
|
105
|
+
"@stacksjs/tunnel": "0.75.36",
|
|
106
|
+
"@stacksjs/types": "0.75.36",
|
|
107
|
+
"@stacksjs/utils": "0.75.36",
|
|
108
|
+
"@stacksjs/whois": "0.75.36",
|
|
109
|
+
"bun-plugin-stx": "^0.2.343"
|
|
110
110
|
},
|
|
111
111
|
"peerDependencies": {
|
|
112
112
|
"craft-native": ">=0.0.103",
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
}
|
|
122
122
|
},
|
|
123
123
|
"devDependencies": {
|
|
124
|
-
"@stacksjs/validation": "0.75.
|
|
124
|
+
"@stacksjs/validation": "0.75.36",
|
|
125
125
|
"better-dx": "^0.2.24"
|
|
126
126
|
}
|
|
127
127
|
}
|