@qkix/strapi-plugin-better-blocks 0.23.1 → 0.23.2
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/README.md +2 -2
- package/dist/admin/src/components/BlocksInput/Blocks/Table.d.ts +1 -1
- package/dist/admin/src/components/BlocksInput/Blocks/embedProviders.d.ts +2 -2
- package/dist/admin/src/components/BlocksInput/Blocks/tableGrid.d.ts +1 -1
- package/dist/admin/src/components/BlocksInput/MenuScrollbar.d.ts +1 -1
- package/dist/admin/src/components/BlocksInput/utils/types.d.ts +4 -4
- package/dist/admin/src/hooks/useDragAndDrop.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.mjs +154 -110
- package/dist/server/src/config/index.d.ts +1 -0
- package/dist/server/src/controllers/oembed.d.ts +1 -1
- package/dist/server/src/index.d.ts +2 -0
- package/dist/server/src/services/index.d.ts +1 -0
- package/dist/server/src/services/oembed.d.ts +44 -1
- package/dist/server/src/services/ttlCache.d.ts +23 -0
- package/package.json +15 -6
package/README.md
CHANGED
|
@@ -94,13 +94,13 @@
|
|
|
94
94
|
- **To-do Lists** — Checkbox list items with click-to-toggle and strikethrough on checked items
|
|
95
95
|
- **Tables** — Insert tables with header row, add/remove rows and columns via hover toolbar
|
|
96
96
|
- **Embeds (generic iframe / URL)** — Insert any third-party embed from the `+` Insert menu, the `/embed` slash command, or the toolbar's media button. Two modes: **URL** (YouTube, Vimeo, Loom, Wistia, Dailymotion and api.video are converted to an iframe automatically) and **Embed code** (paste any platform's `<iframe>` snippet). The pasted markup is never stored verbatim — the iframe is _rebuilt_ from an attribute allowlist over an https-only `src`, so scripts, event handlers and unknown attributes cannot survive into your frontend. Live preview in the editor, plus aspect ratio (16:9 / 21:9 / 4:3 / 1:1 / custom), alignment, caption and an accessible title. Supersedes the older YouTube/Vimeo-only **media embed** block, which still renders existing content but is no longer inserted
|
|
97
|
-
- **Video (provider-aware)** — Insert a video from the `+` Insert menu or the `/video` slash command. Source it from the **Media Library**, a **direct URL**, or a hosting provider — Mux, api.video and Cloudinary URLs are detected automatically, and a bare **Mux playback ID** fills in the stream URL and poster frame for you. When [`strapi-plugin-mux-video-uploader`](https://github.com/muxinc/strapi-plugin-mux-video-uploader) is installed and configured, a **Mux** button lists and searches your Mux assets inline with thumbnails. Set a poster image, title, caption, WebVTT captions/transcript, alignment, aspect ratio, and player
|
|
97
|
+
- **Video (provider-aware)** — Insert a video from the `+` Insert menu or the `/video` slash command. Source it from the **Media Library**, a **direct URL**, or a hosting provider — Mux, api.video and Cloudinary URLs are detected automatically, and a bare **Mux playback ID** fills in the stream URL and poster frame for you. When [`strapi-plugin-mux-video-uploader`](https://github.com/muxinc/strapi-plugin-mux-video-uploader) is installed and configured, a **Mux** button lists and searches your Mux assets inline with thumbnails. Set a poster image, title, caption, WebVTT captions/transcript, alignment, aspect ratio, and player behavior (controls, autoplay, muted, loop)
|
|
98
98
|
- **Math (LaTeX / KaTeX)** — Inline and block math rendered with KaTeX; insert from the toolbar, the `/math` slash command, the blocks selector, or by typing `$$ ` (block) / `$…$ ` (inline), then edit in a full-screen modal with a side-by-side source editor and live preview. Block math supports multi-line equations via `\\` and LaTeX environments such as `aligned` and `cases`
|
|
99
99
|
- **Diagrams (Mermaid)** — Block-level [Mermaid](https://mermaid.js.org/) diagrams (flowcharts, sequence, class, state, ER, pie, and more) rendered to SVG; insert from the blocks selector, the `/mermaid` slash command, or by typing ` ```mermaid ` then a space, then edit the definition in a full-screen modal with live preview and zoom controls. Theme follows Strapi's light/dark mode
|
|
100
100
|
- **Callouts / Admonitions** — GitHub-style callouts in five variants (`Note`, `Tip`, `Important`, `Warning`, `Caution`) with an optional custom title and nested rich-text content (paragraphs, lists, links). Insert from the blocks selector or the `/note`, `/tip`, `/important`, `/warning`, `/caution` slash commands; switch variant, edit the title, or remove from the header popover. Colors follow Strapi's design tokens and adapt to light/dark mode
|
|
101
101
|
- **Details / Collapsible** — GitHub-style collapsible `<details>` / `<summary>` sections for managing content density. Insert from the blocks selector or the `/details` slash command; edit the summary label and toggle open/closed-by-default (`defaultOpen`) from the header. Holds full rich-text block content (paragraphs, lists, tables, images) and supports nesting. Admins can set the default summary text and choose a GitHub-minimal (default) or Custom (bordered + background) style. Stored as `{ "type": "details", "summary": "…", "defaultOpen": false, "children": [...] }`
|
|
102
102
|
- **Button (WordPress-style CTA)** — Insert a styled call-to-action button from the blocks selector or by typing `[button]` then a space. Two modes: **Link** (URL + open-in-new-tab + ARIA label) and **File** (pick any asset from the Media Library to render a download button with optional file size and type icon, and choose direct download or preview-in-new-tab). A full-screen editor with live preview controls alignment, background/text colors (including hover colors), border radius, font size/weight, padding presets, border, and a custom CSS class. One-click **style presets** (Primary / Secondary / Outline / Filled) keep CTAs on-brand. Admins can set default button colors and customize the presets. Stored as `{ "type": "button", "buttonType": "link" | "file", "label": "…", "alignment": "center", "link": {…} | "file": {…}, "style": {…} }`
|
|
103
|
-
- **Audio (HTML5 player)** — Insert an audio block from the `+` Insert menu, the `/audio` slash command, or by typing `[audio]` then a space. Pick a file from the Media Library (upload included) or paste a direct URL, with a live native `<audio>` preview in the editor. Set an optional title and caption, block alignment (left / center / right / none), and player
|
|
103
|
+
- **Audio (HTML5 player)** — Insert an audio block from the `+` Insert menu, the `/audio` slash command, or by typing `[audio]` then a space. Pick a file from the Media Library (upload included) or paste a direct URL, with a live native `<audio>` preview in the editor. Set an optional title and caption, block alignment (left / center / right / none), and player behavior: controls, autoplay, loop, and preload (none / metadata / auto). Stored as `{ "type": "audio", "file": {…}, "title": "…", "caption": "…", "player": {…}, "alignment": "center" }`
|
|
104
104
|
- **Horizontal Line** — Insert `<hr>` dividers between content blocks
|
|
105
105
|
- **Text Alignment** — Per-block left, center, right, and justify alignment
|
|
106
106
|
- **Undo / Redo** — Toolbar buttons wired to Slate's built-in history
|
|
@@ -2,7 +2,7 @@ import { Editor } from 'slate';
|
|
|
2
2
|
import { type BlocksStore } from '../BlocksEditor';
|
|
3
3
|
import { createTableCell, createTableRow, insertTable } from './tableOperations';
|
|
4
4
|
/**
|
|
5
|
-
* Protects the table's shape from Slate's generic editing
|
|
5
|
+
* Protects the table's shape from Slate's generic editing behaviors (deleting
|
|
6
6
|
* across cells, normalizing rows away, converting a cell to a paragraph, …).
|
|
7
7
|
*
|
|
8
8
|
* Structural edits are performed by tableOperations, which lifts the guard via
|
|
@@ -3,7 +3,7 @@ import { type AspectRatio, type EmbedProvider } from '../utils/types';
|
|
|
3
3
|
export declare const EMBED_FRAME_HOSTS: string[];
|
|
4
4
|
/** Human-readable provider name for a URL, e.g. "YouTube". */
|
|
5
5
|
export declare const getProviderLabel: (provider?: EmbedProvider) => string;
|
|
6
|
-
/** Detected provider for a share URL, or `generic` when
|
|
6
|
+
/** Detected provider for a share URL, or `generic` when unrecognized. */
|
|
7
7
|
export declare const detectProvider: (url: string) => EmbedProvider;
|
|
8
8
|
/** Iframe `src` derived from a share URL, or null when not derivable. */
|
|
9
9
|
export declare const getEmbedSrc: (url: string) => string | null;
|
|
@@ -32,7 +32,7 @@ export interface IframeError {
|
|
|
32
32
|
*/
|
|
33
33
|
export declare const sanitizeIframe: (input: string) => SanitizedIframe | IframeError;
|
|
34
34
|
/**
|
|
35
|
-
* Build the iframe markup for a
|
|
35
|
+
* Build the iframe markup for a recognized share URL. Returns null when the URL
|
|
36
36
|
* isn't one of the known providers — the author then has to use embed-code mode.
|
|
37
37
|
*/
|
|
38
38
|
export declare const buildUrlEmbed: (url: string, title?: string) => SanitizedIframe | null;
|
|
@@ -85,6 +85,6 @@ export declare const cellsInRange: (grid: TableGrid, range: GridRange) => GridCe
|
|
|
85
85
|
* linear, so everything between r2c1 and r3c2 includes the whole tail of row 2
|
|
86
86
|
* — taking the bounding box of every touched cell would turn a 2x2 pick into a
|
|
87
87
|
* full-width 2x5 one. Treating anchor and focus as opposite corners gives the
|
|
88
|
-
* 2D
|
|
88
|
+
* 2D behavior authors expect from a table.
|
|
89
89
|
*/
|
|
90
90
|
export declare const getSelectedRange: (editor: Editor, grid: TableGrid) => GridRange | null;
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* scroll (defeating the purpose on macOS). We therefore do NOT set
|
|
21
21
|
* `scrollbar-color`; Firefox falls back to its own always-visible bar.
|
|
22
22
|
* - The track gets a real background so the channel stays visible even when the
|
|
23
|
-
* content fits and the thumb spans the full height; the
|
|
23
|
+
* content fits and the thumb spans the full height; the colors are
|
|
24
24
|
* mid-range neutrals that contrast with the menu background (`neutral0`) in
|
|
25
25
|
* both the light and dark themes.
|
|
26
26
|
* - `!important` / `display: block` are required to beat the design-system
|
|
@@ -85,7 +85,7 @@ export interface ImageElement extends CustomElement {
|
|
|
85
85
|
}
|
|
86
86
|
export type AudioAlignment = 'left' | 'center' | 'right' | 'none';
|
|
87
87
|
export type AudioPreload = 'none' | 'metadata' | 'auto';
|
|
88
|
-
/** Player
|
|
88
|
+
/** Player behavior flags, mirrored 1:1 onto the HTML5 `<audio>` element. */
|
|
89
89
|
export interface AudioPlayerSettings {
|
|
90
90
|
autoplay: boolean;
|
|
91
91
|
loop: boolean;
|
|
@@ -95,7 +95,7 @@ export interface AudioPlayerSettings {
|
|
|
95
95
|
/**
|
|
96
96
|
* Audio block. A void block referencing a Strapi Media Library asset (or an
|
|
97
97
|
* external URL). Stores the file metadata plus a display title, caption, player
|
|
98
|
-
*
|
|
98
|
+
* behavior flags and alignment. The frontend renderer turns this into a native
|
|
99
99
|
* HTML5 `<audio>` player — see issue #43 for the renderer contract.
|
|
100
100
|
*/
|
|
101
101
|
export interface AudioElement extends CustomElement {
|
|
@@ -271,7 +271,7 @@ export declare const isButtonNode: (element: CustomElement) => element is Button
|
|
|
271
271
|
export type SocialPlatform = 'twitter' | 'instagram' | 'facebook' | 'tiktok' | 'linkedin' | 'pinterest';
|
|
272
272
|
export type SocialAlignment = 'left' | 'center' | 'right';
|
|
273
273
|
/**
|
|
274
|
-
*
|
|
274
|
+
* Normalized oEmbed payload fetched from the platform via the plugin's server
|
|
275
275
|
* oEmbed proxy (`GET /better-blocks/oembed`). Stored on the node so the frontend
|
|
276
276
|
* renderer can render the embed without itself calling the platform at runtime.
|
|
277
277
|
*/
|
|
@@ -353,7 +353,7 @@ export interface EmbedElement extends CustomElement {
|
|
|
353
353
|
export declare const isEmbedNode: (element: CustomElement) => element is EmbedElement;
|
|
354
354
|
/** Video hosting providers the video block understands. */
|
|
355
355
|
export type VideoProvider = 'local' | 'mux' | 'api-video' | 'cloudinary' | 'custom';
|
|
356
|
-
/** Player
|
|
356
|
+
/** Player behavior flags, mirrored onto the HTML5 `<video>` element. */
|
|
357
357
|
export interface VideoPlayerSettings {
|
|
358
358
|
autoplay: boolean;
|
|
359
359
|
loop: boolean;
|
|
@@ -38,7 +38,7 @@ type UseDragAndDropReturn<E extends Element = HTMLElement> = [
|
|
|
38
38
|
];
|
|
39
39
|
/**
|
|
40
40
|
* A utility hook abstracting the general drag and drop hooks from react-dnd.
|
|
41
|
-
* Centralising the same
|
|
41
|
+
* Centralising the same behaviors and by default offering keyboard support.
|
|
42
42
|
*/
|
|
43
43
|
declare const useDragAndDrop: <TIndex extends number | number[], TItem extends {
|
|
44
44
|
[key: string]: unknown;
|
package/dist/server/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";const
|
|
1
|
+
"use strict";const k=({strapi:o})=>{},w=({strapi:o})=>{},y=({strapi:o})=>{o.customFields.register({name:"better-blocks",plugin:"better-blocks",type:"json",inputSize:{default:12,isResizable:!1}})},C={default:{details:{defaultSummary:"Click to expand",style:"github"},button:{defaultStyle:{backgroundColor:"#4945ff",textColor:"#ffffff",borderRadius:"4px",padding:"12px 24px",fontSize:"16px",fontWeight:"600"},presets:{primary:{backgroundColor:"#4945ff",textColor:"#ffffff",border:"none"},secondary:{backgroundColor:"#dcdce4",textColor:"#32324d",border:"none"},outline:{backgroundColor:"transparent",textColor:"#4945ff",border:"2px solid #4945ff"},filled:{backgroundColor:"#32324d",textColor:"#ffffff",border:"none"}}},social:{enabled:!0,platforms:["twitter","instagram","facebook","tiktok","linkedin","pinterest"],cache:!0,cacheTTL:86400,cacheMaxEntries:500,instagram:{accessToken:void 0},facebook:{accessToken:void 0}}},validator(o={}){const t=o?.details?.style;if(t&&t!=="github"&&t!=="custom")throw new Error(`[better-blocks] config.details.style must be "github" or "custom" (received "${t}")`)}},v={},x=({strapi:o})=>({index(t){t.body=o.plugin("better-blocks").service("service").getWelcomeMessage()},getConfig(t){t.body={details:o.plugin("better-blocks").config("details",{defaultSummary:"Click to expand",style:"github"}),button:o.plugin("better-blocks").config("button",{defaultStyle:{backgroundColor:"#4945ff",textColor:"#ffffff",borderRadius:"4px",padding:"12px 24px",fontSize:"16px",fontWeight:"600"},presets:{primary:{backgroundColor:"#4945ff",textColor:"#ffffff",border:"none"},secondary:{backgroundColor:"#dcdce4",textColor:"#32324d",border:"none"},outline:{backgroundColor:"transparent",textColor:"#4945ff",border:"2px solid #4945ff"},filled:{backgroundColor:"#32324d",textColor:"#ffffff",border:"none"}}}),social:(()=>{const e=o.plugin("better-blocks").config("social",{enabled:!0,platforms:["twitter","instagram","facebook","tiktok","linkedin","pinterest"]});return{enabled:e.enabled!==!1,platforms:e.platforms??[],instagramConfigured:!!e.instagram?.accessToken,facebookConfigured:!!e.facebook?.accessToken}})()}}}),E=["twitter","instagram","facebook","tiktok","linkedin","pinterest"],$=({strapi:o})=>({async fetch(t){const{url:e,platform:r}=t.query;if(!e||typeof e!="string")return t.badRequest('Missing required "url" query parameter.');const n=r&&E.includes(r)?r:void 0;try{t.body=await o.plugin("better-blocks").service("oembed").fetch(e,n)}catch(s){const c=s instanceof Error?s.message:"oEmbed request failed.";return t.badRequest(c)}}}),T={controller:x,oembed:$},R={},_={},L=[{method:"GET",path:"/",handler:"controller.index",config:{policies:[]}}],S=[{method:"GET",path:"/config",handler:"controller.getConfig",config:{policies:[]}},{method:"GET",path:"/oembed",handler:"oembed.fetch",config:{policies:["admin::isAuthenticatedAdmin"]}}],z={"content-api":{type:"content-api",routes:L},admin:{type:"admin",routes:S}},A=({strapi:o})=>({getWelcomeMessage(){return"Welcome to Strapi 🚀"}}),U=({max:o})=>{if(o<1)throw new Error(`ttlCache: max must be at least 1 (received ${o})`);const t=new Map;return{get(e){const r=t.get(e);if(r){if(r.expires<=Date.now()){t.delete(e);return}return t.delete(e),t.set(e,r),r.value}},set(e,r,n){if(t.delete(e),t.size>=o){const s=Date.now();for(const[c,i]of t)i.expires<=s&&t.delete(c)}for(;t.size>=o;){const s=t.keys().next();if(s.done)break;t.delete(s.value)}t.set(e,{expires:Date.now()+n,value:r})},delete:e=>t.delete(e),clear:()=>t.clear(),get size(){return t.size}}},I=[{platform:"twitter",re:/(?:twitter\.com|x\.com)\/[^/]+\/status(?:es)?\/\d+/i},{platform:"instagram",re:/instagram\.com\/(?:p|reel|tv)\//i},{platform:"tiktok",re:/tiktok\.com\/@[^/]+\/video\/\d+/i},{platform:"facebook",re:/facebook\.com\//i},{platform:"linkedin",re:/linkedin\.com\//i},{platform:"pinterest",re:/(?:pinterest\.[a-z.]+|pin\.it)\//i}],g=o=>{const t=I.find(({re:e})=>e.test(o));return t?t.platform:null},M=(o,t,e)=>{const r=encodeURIComponent(t);switch(o){case"twitter":return`https://publish.twitter.com/oembed?url=${r}&omit_script=true&dnt=true`;case"tiktok":return`https://www.tiktok.com/oembed?url=${r}`;case"pinterest":return`https://www.pinterest.com/oembed.json?url=${r}`;case"instagram":{const n=e.instagram?.accessToken;return n?`https://graph.facebook.com/v17.0/instagram_oembed?url=${r}&access_token=${encodeURIComponent(n)}&omitscript=true`:null}case"facebook":{const n=e.facebook?.accessToken;return n?`https://graph.facebook.com/v17.0/oembed_post?url=${r}&access_token=${encodeURIComponent(n)}&omitscript=true`:null}case"linkedin":return null;default:return null}},P=async o=>{if(!/^https?:\/\/pin\.it\//i.test(o))return o;try{const e=(await fetch(o,{redirect:"follow"})).url||o;return/pinterest\.[a-z.]+\/pin\//i.test(e)?e:o}catch{return o}},q=o=>{const t=o.match(/(?:urn:li:)?(ugcPost|activity|share)[:-](\d{10,})/i);if(!t)return null;const r=`urn:li:${t[1].toLowerCase()==="ugcpost"?"ugcPost":t[1].toLowerCase()}:${t[2]}`,n=`https://www.linkedin.com/embed/feed/update/${encodeURIComponent(r)}`;return{platform:"linkedin",url:o,html:`<iframe src="${n}" height="600" width="100%" frameborder="0" allowfullscreen title="Embedded LinkedIn post" loading="lazy"></iframe>`,providerName:"LinkedIn",width:504,height:600}},D=o=>o.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi,"").trim(),W=(o,t,e)=>({platform:o,url:t,html:typeof e.html=="string"&&D(e.html)||null,title:typeof e.title=="string"?e.title:void 0,author:typeof e.author_name=="string"?e.author_name:void 0,authorUrl:typeof e.author_url=="string"?e.author_url:void 0,thumbnailUrl:typeof e.thumbnail_url=="string"?e.thumbnail_url:void 0,providerName:typeof e.provider_name=="string"?e.provider_name:void 0,width:typeof e.width=="number"?e.width:void 0,height:typeof e.height=="number"?e.height:void 0}),j=500,F=({strapi:o})=>{const t=()=>o.plugin("better-blocks").config("social",{});let e=null;const r=()=>(e||(e=U({max:t().cacheMaxEntries??j})),e);return{detectPlatform:g,clearCache:()=>e?.clear(),async fetch(n,s){const c=t();if(c.enabled===!1)throw new Error("Social embeds are disabled in plugin config.");const i=s??g(n);if(!i)throw new Error(`Unsupported or unrecognized social URL: ${n}`);const u=c.platforms;if(Array.isArray(u)&&!u.includes(i))throw new Error(`Platform "${i}" is not enabled in plugin config.`);const p=c.cache!==!1,m=(c.cacheTTL??86400)*1e3,b=`${i}:${n}`;if(p){const l=r().get(b);if(l)return l}let a=null;if(i==="linkedin"){if(a=q(n),!a)throw new Error("Could not derive a LinkedIn embed from this URL. Paste the post embed code instead.")}else{const l=await P(n),h=M(i,l,c);if(!h)throw new Error(`oEmbed for "${i}" requires an access token. Configure config.social.${i}.accessToken, or paste the embed code instead.`);const d=await fetch(h,{headers:{Accept:"application/json"}});if(!d.ok)throw new Error(`oEmbed request to ${i} failed (HTTP ${d.status}).`);const f=await d.json();if(typeof f.error=="string")throw new Error(`${i} could not embed this URL: ${f.error}`);if(a=W(i,l,f),!a.html)throw new Error(`${i} returned no embed markup for this URL. Check the post is public, or paste the embed code instead.`)}return p&&m>0&&r().set(b,a,m),a}}},N={service:A,oembed:F},G={register:y,bootstrap:k,destroy:w,config:C,controllers:T,routes:z,services:N,contentTypes:v,policies:_,middlewares:R};module.exports=G;
|
package/dist/server/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
const
|
|
2
|
-
},
|
|
3
|
-
}, y = ({ strapi:
|
|
4
|
-
|
|
1
|
+
const k = ({ strapi: o }) => {
|
|
2
|
+
}, w = ({ strapi: o }) => {
|
|
3
|
+
}, y = ({ strapi: o }) => {
|
|
4
|
+
o.customFields.register({
|
|
5
5
|
name: "better-blocks",
|
|
6
6
|
plugin: "better-blocks",
|
|
7
7
|
type: "json",
|
|
@@ -10,7 +10,7 @@ const g = ({ strapi: t }) => {
|
|
|
10
10
|
isResizable: !1
|
|
11
11
|
}
|
|
12
12
|
});
|
|
13
|
-
},
|
|
13
|
+
}, C = {
|
|
14
14
|
default: {
|
|
15
15
|
/**
|
|
16
16
|
* Defaults for the collapsible Details / Summary block.
|
|
@@ -97,32 +97,36 @@ const g = ({ strapi: t }) => {
|
|
|
97
97
|
cache: !0,
|
|
98
98
|
cacheTTL: 86400,
|
|
99
99
|
// seconds (24h)
|
|
100
|
+
// Upper bound on cached embeds, evicted least-recently-used. Without one
|
|
101
|
+
// the cache keeps an entry for every post ever embedded, for the life of
|
|
102
|
+
// the process — expiry alone never frees anything nobody asks for again.
|
|
103
|
+
cacheMaxEntries: 500,
|
|
100
104
|
instagram: { accessToken: void 0 },
|
|
101
105
|
facebook: { accessToken: void 0 }
|
|
102
106
|
}
|
|
103
107
|
},
|
|
104
|
-
validator(
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
108
|
+
validator(o = {}) {
|
|
109
|
+
const t = o?.details?.style;
|
|
110
|
+
if (t && t !== "github" && t !== "custom")
|
|
107
111
|
throw new Error(
|
|
108
|
-
`[better-blocks] config.details.style must be "github" or "custom" (received "${
|
|
112
|
+
`[better-blocks] config.details.style must be "github" or "custom" (received "${t}")`
|
|
109
113
|
);
|
|
110
114
|
}
|
|
111
|
-
},
|
|
112
|
-
index(
|
|
113
|
-
|
|
115
|
+
}, v = {}, x = ({ strapi: o }) => ({
|
|
116
|
+
index(t) {
|
|
117
|
+
t.body = o.plugin("better-blocks").service("service").getWelcomeMessage();
|
|
114
118
|
},
|
|
115
119
|
/**
|
|
116
120
|
* Exposes the plugin config (config/plugins.js merged with server defaults) to
|
|
117
121
|
* the admin panel, so blocks like Details can honour admin-level settings.
|
|
118
122
|
*/
|
|
119
|
-
getConfig(
|
|
120
|
-
|
|
121
|
-
details:
|
|
123
|
+
getConfig(t) {
|
|
124
|
+
t.body = {
|
|
125
|
+
details: o.plugin("better-blocks").config("details", {
|
|
122
126
|
defaultSummary: "Click to expand",
|
|
123
127
|
style: "github"
|
|
124
128
|
}),
|
|
125
|
-
button:
|
|
129
|
+
button: o.plugin("better-blocks").config("button", {
|
|
126
130
|
defaultStyle: {
|
|
127
131
|
backgroundColor: "#4945ff",
|
|
128
132
|
textColor: "#ffffff",
|
|
@@ -155,7 +159,7 @@ const g = ({ strapi: t }) => {
|
|
|
155
159
|
}
|
|
156
160
|
}),
|
|
157
161
|
social: (() => {
|
|
158
|
-
const e =
|
|
162
|
+
const e = o.plugin("better-blocks").config("social", {
|
|
159
163
|
enabled: !0,
|
|
160
164
|
platforms: [
|
|
161
165
|
"twitter",
|
|
@@ -175,35 +179,35 @@ const g = ({ strapi: t }) => {
|
|
|
175
179
|
})()
|
|
176
180
|
};
|
|
177
181
|
}
|
|
178
|
-
}),
|
|
182
|
+
}), E = [
|
|
179
183
|
"twitter",
|
|
180
184
|
"instagram",
|
|
181
185
|
"facebook",
|
|
182
186
|
"tiktok",
|
|
183
187
|
"linkedin",
|
|
184
188
|
"pinterest"
|
|
185
|
-
], $ = ({ strapi:
|
|
189
|
+
], $ = ({ strapi: o }) => ({
|
|
186
190
|
/**
|
|
187
191
|
* GET /better-blocks/oembed?url=...&platform=...
|
|
188
|
-
* Proxies the platform oEmbed request server-side and returns the
|
|
192
|
+
* Proxies the platform oEmbed request server-side and returns the normalized
|
|
189
193
|
* payload used by the social-embed block.
|
|
190
194
|
*/
|
|
191
|
-
async fetch(
|
|
192
|
-
const { url: e, platform:
|
|
195
|
+
async fetch(t) {
|
|
196
|
+
const { url: e, platform: r } = t.query;
|
|
193
197
|
if (!e || typeof e != "string")
|
|
194
|
-
return
|
|
195
|
-
const
|
|
198
|
+
return t.badRequest('Missing required "url" query parameter.');
|
|
199
|
+
const n = r && E.includes(r) ? r : void 0;
|
|
196
200
|
try {
|
|
197
|
-
|
|
198
|
-
} catch (
|
|
199
|
-
const
|
|
200
|
-
return
|
|
201
|
+
t.body = await o.plugin("better-blocks").service("oembed").fetch(e, n);
|
|
202
|
+
} catch (s) {
|
|
203
|
+
const c = s instanceof Error ? s.message : "oEmbed request failed.";
|
|
204
|
+
return t.badRequest(c);
|
|
201
205
|
}
|
|
202
206
|
}
|
|
203
207
|
}), T = {
|
|
204
|
-
controller:
|
|
208
|
+
controller: x,
|
|
205
209
|
oembed: $
|
|
206
|
-
},
|
|
210
|
+
}, R = {}, _ = {}, L = [
|
|
207
211
|
{
|
|
208
212
|
method: "GET",
|
|
209
213
|
path: "/",
|
|
@@ -213,7 +217,7 @@ const g = ({ strapi: t }) => {
|
|
|
213
217
|
policies: []
|
|
214
218
|
}
|
|
215
219
|
}
|
|
216
|
-
],
|
|
220
|
+
], S = [
|
|
217
221
|
{
|
|
218
222
|
method: "GET",
|
|
219
223
|
path: "/config",
|
|
@@ -232,20 +236,54 @@ const g = ({ strapi: t }) => {
|
|
|
232
236
|
policies: ["admin::isAuthenticatedAdmin"]
|
|
233
237
|
}
|
|
234
238
|
}
|
|
235
|
-
],
|
|
239
|
+
], z = {
|
|
236
240
|
"content-api": {
|
|
237
241
|
type: "content-api",
|
|
238
242
|
routes: L
|
|
239
243
|
},
|
|
240
244
|
admin: {
|
|
241
245
|
type: "admin",
|
|
242
|
-
routes:
|
|
246
|
+
routes: S
|
|
243
247
|
}
|
|
244
|
-
},
|
|
248
|
+
}, A = ({ strapi: o }) => ({
|
|
245
249
|
getWelcomeMessage() {
|
|
246
250
|
return "Welcome to Strapi 🚀";
|
|
247
251
|
}
|
|
248
|
-
}),
|
|
252
|
+
}), U = ({ max: o }) => {
|
|
253
|
+
if (o < 1)
|
|
254
|
+
throw new Error(`ttlCache: max must be at least 1 (received ${o})`);
|
|
255
|
+
const t = /* @__PURE__ */ new Map();
|
|
256
|
+
return {
|
|
257
|
+
get(e) {
|
|
258
|
+
const r = t.get(e);
|
|
259
|
+
if (r) {
|
|
260
|
+
if (r.expires <= Date.now()) {
|
|
261
|
+
t.delete(e);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
return t.delete(e), t.set(e, r), r.value;
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
set(e, r, n) {
|
|
268
|
+
if (t.delete(e), t.size >= o) {
|
|
269
|
+
const s = Date.now();
|
|
270
|
+
for (const [c, i] of t)
|
|
271
|
+
i.expires <= s && t.delete(c);
|
|
272
|
+
}
|
|
273
|
+
for (; t.size >= o; ) {
|
|
274
|
+
const s = t.keys().next();
|
|
275
|
+
if (s.done) break;
|
|
276
|
+
t.delete(s.value);
|
|
277
|
+
}
|
|
278
|
+
t.set(e, { expires: Date.now() + n, value: r });
|
|
279
|
+
},
|
|
280
|
+
delete: (e) => t.delete(e),
|
|
281
|
+
clear: () => t.clear(),
|
|
282
|
+
get size() {
|
|
283
|
+
return t.size;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}, I = [
|
|
249
287
|
{
|
|
250
288
|
platform: "twitter",
|
|
251
289
|
re: /(?:twitter\.com|x\.com)\/[^/]+\/status(?:es)?\/\d+/i
|
|
@@ -255,28 +293,28 @@ const g = ({ strapi: t }) => {
|
|
|
255
293
|
{ platform: "facebook", re: /facebook\.com\//i },
|
|
256
294
|
{ platform: "linkedin", re: /linkedin\.com\//i },
|
|
257
295
|
{ platform: "pinterest", re: /(?:pinterest\.[a-z.]+|pin\.it)\//i }
|
|
258
|
-
],
|
|
259
|
-
const
|
|
260
|
-
return
|
|
261
|
-
},
|
|
262
|
-
const
|
|
263
|
-
switch (
|
|
296
|
+
], g = (o) => {
|
|
297
|
+
const t = I.find(({ re: e }) => e.test(o));
|
|
298
|
+
return t ? t.platform : null;
|
|
299
|
+
}, M = (o, t, e) => {
|
|
300
|
+
const r = encodeURIComponent(t);
|
|
301
|
+
switch (o) {
|
|
264
302
|
case "twitter":
|
|
265
|
-
return `https://publish.twitter.com/oembed?url=${
|
|
303
|
+
return `https://publish.twitter.com/oembed?url=${r}&omit_script=true&dnt=true`;
|
|
266
304
|
case "tiktok":
|
|
267
|
-
return `https://www.tiktok.com/oembed?url=${
|
|
305
|
+
return `https://www.tiktok.com/oembed?url=${r}`;
|
|
268
306
|
case "pinterest":
|
|
269
|
-
return `https://www.pinterest.com/oembed.json?url=${
|
|
307
|
+
return `https://www.pinterest.com/oembed.json?url=${r}`;
|
|
270
308
|
case "instagram": {
|
|
271
|
-
const
|
|
272
|
-
return
|
|
273
|
-
|
|
309
|
+
const n = e.instagram?.accessToken;
|
|
310
|
+
return n ? `https://graph.facebook.com/v17.0/instagram_oembed?url=${r}&access_token=${encodeURIComponent(
|
|
311
|
+
n
|
|
274
312
|
)}&omitscript=true` : null;
|
|
275
313
|
}
|
|
276
314
|
case "facebook": {
|
|
277
|
-
const
|
|
278
|
-
return
|
|
279
|
-
|
|
315
|
+
const n = e.facebook?.accessToken;
|
|
316
|
+
return n ? `https://graph.facebook.com/v17.0/oembed_post?url=${r}&access_token=${encodeURIComponent(
|
|
317
|
+
n
|
|
280
318
|
)}&omitscript=true` : null;
|
|
281
319
|
}
|
|
282
320
|
case "linkedin":
|
|
@@ -284,32 +322,32 @@ const g = ({ strapi: t }) => {
|
|
|
284
322
|
default:
|
|
285
323
|
return null;
|
|
286
324
|
}
|
|
287
|
-
}, P = async (
|
|
288
|
-
if (!/^https?:\/\/pin\.it\//i.test(
|
|
325
|
+
}, P = async (o) => {
|
|
326
|
+
if (!/^https?:\/\/pin\.it\//i.test(o)) return o;
|
|
289
327
|
try {
|
|
290
|
-
const e = (await fetch(
|
|
291
|
-
return /pinterest\.[a-z.]+\/pin\//i.test(e) ? e :
|
|
328
|
+
const e = (await fetch(o, { redirect: "follow" })).url || o;
|
|
329
|
+
return /pinterest\.[a-z.]+\/pin\//i.test(e) ? e : o;
|
|
292
330
|
} catch {
|
|
293
|
-
return
|
|
331
|
+
return o;
|
|
294
332
|
}
|
|
295
|
-
}, q = (
|
|
296
|
-
const
|
|
333
|
+
}, q = (o) => {
|
|
334
|
+
const t = o.match(
|
|
297
335
|
/(?:urn:li:)?(ugcPost|activity|share)[:-](\d{10,})/i
|
|
298
336
|
);
|
|
299
|
-
if (!
|
|
300
|
-
const
|
|
337
|
+
if (!t) return null;
|
|
338
|
+
const r = `urn:li:${t[1].toLowerCase() === "ugcpost" ? "ugcPost" : t[1].toLowerCase()}:${t[2]}`, n = `https://www.linkedin.com/embed/feed/update/${encodeURIComponent(r)}`;
|
|
301
339
|
return {
|
|
302
340
|
platform: "linkedin",
|
|
303
|
-
url:
|
|
304
|
-
html: `<iframe src="${
|
|
341
|
+
url: o,
|
|
342
|
+
html: `<iframe src="${n}" height="600" width="100%" frameborder="0" allowfullscreen title="Embedded LinkedIn post" loading="lazy"></iframe>`,
|
|
305
343
|
providerName: "LinkedIn",
|
|
306
344
|
width: 504,
|
|
307
345
|
height: 600
|
|
308
346
|
};
|
|
309
|
-
},
|
|
310
|
-
platform:
|
|
311
|
-
url:
|
|
312
|
-
html: typeof e.html == "string" &&
|
|
347
|
+
}, D = (o) => o.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "").trim(), W = (o, t, e) => ({
|
|
348
|
+
platform: o,
|
|
349
|
+
url: t,
|
|
350
|
+
html: typeof e.html == "string" && D(e.html) || null,
|
|
313
351
|
title: typeof e.title == "string" ? e.title : void 0,
|
|
314
352
|
author: typeof e.author_name == "string" ? e.author_name : void 0,
|
|
315
353
|
authorUrl: typeof e.author_url == "string" ? e.author_url : void 0,
|
|
@@ -317,77 +355,83 @@ const g = ({ strapi: t }) => {
|
|
|
317
355
|
providerName: typeof e.provider_name == "string" ? e.provider_name : void 0,
|
|
318
356
|
width: typeof e.width == "number" ? e.width : void 0,
|
|
319
357
|
height: typeof e.height == "number" ? e.height : void 0
|
|
320
|
-
}),
|
|
321
|
-
const
|
|
358
|
+
}), j = 500, F = ({ strapi: o }) => {
|
|
359
|
+
const t = () => o.plugin("better-blocks").config("social", {});
|
|
360
|
+
let e = null;
|
|
361
|
+
const r = () => (e || (e = U({
|
|
362
|
+
max: t().cacheMaxEntries ?? j
|
|
363
|
+
})), e);
|
|
322
364
|
return {
|
|
323
|
-
detectPlatform:
|
|
365
|
+
detectPlatform: g,
|
|
366
|
+
/** Exposed for tests and for anyone who needs to drop cached embeds. */
|
|
367
|
+
clearCache: () => e?.clear(),
|
|
324
368
|
/**
|
|
325
|
-
* Fetch (and cache) the
|
|
369
|
+
* Fetch (and cache) the normalized oEmbed for `url`. `platform` may be
|
|
326
370
|
* passed to skip auto-detection. Throws on an unsupported/unconfigured
|
|
327
371
|
* platform or a failed upstream request.
|
|
328
372
|
*/
|
|
329
|
-
async fetch(
|
|
330
|
-
const
|
|
331
|
-
if (
|
|
373
|
+
async fetch(n, s) {
|
|
374
|
+
const c = t();
|
|
375
|
+
if (c.enabled === !1)
|
|
332
376
|
throw new Error("Social embeds are disabled in plugin config.");
|
|
333
|
-
const
|
|
334
|
-
if (!
|
|
335
|
-
throw new Error(`Unsupported or
|
|
336
|
-
const
|
|
337
|
-
if (Array.isArray(
|
|
377
|
+
const i = s ?? g(n);
|
|
378
|
+
if (!i)
|
|
379
|
+
throw new Error(`Unsupported or unrecognized social URL: ${n}`);
|
|
380
|
+
const u = c.platforms;
|
|
381
|
+
if (Array.isArray(u) && !u.includes(i))
|
|
338
382
|
throw new Error(
|
|
339
|
-
`Platform "${
|
|
383
|
+
`Platform "${i}" is not enabled in plugin config.`
|
|
340
384
|
);
|
|
341
|
-
const
|
|
342
|
-
if (
|
|
343
|
-
const
|
|
344
|
-
if (
|
|
385
|
+
const p = c.cache !== !1, m = (c.cacheTTL ?? 86400) * 1e3, b = `${i}:${n}`;
|
|
386
|
+
if (p) {
|
|
387
|
+
const l = r().get(b);
|
|
388
|
+
if (l) return l;
|
|
345
389
|
}
|
|
346
|
-
let
|
|
347
|
-
if (
|
|
348
|
-
if (
|
|
390
|
+
let a = null;
|
|
391
|
+
if (i === "linkedin") {
|
|
392
|
+
if (a = q(n), !a)
|
|
349
393
|
throw new Error(
|
|
350
394
|
"Could not derive a LinkedIn embed from this URL. Paste the post embed code instead."
|
|
351
395
|
);
|
|
352
396
|
} else {
|
|
353
|
-
const
|
|
354
|
-
if (!
|
|
397
|
+
const l = await P(n), h = M(i, l, c);
|
|
398
|
+
if (!h)
|
|
355
399
|
throw new Error(
|
|
356
|
-
`oEmbed for "${
|
|
400
|
+
`oEmbed for "${i}" requires an access token. Configure config.social.${i}.accessToken, or paste the embed code instead.`
|
|
357
401
|
);
|
|
358
|
-
const
|
|
402
|
+
const d = await fetch(h, {
|
|
359
403
|
headers: { Accept: "application/json" }
|
|
360
404
|
});
|
|
361
|
-
if (!
|
|
405
|
+
if (!d.ok)
|
|
362
406
|
throw new Error(
|
|
363
|
-
`oEmbed request to ${
|
|
407
|
+
`oEmbed request to ${i} failed (HTTP ${d.status}).`
|
|
364
408
|
);
|
|
365
|
-
const
|
|
366
|
-
if (typeof
|
|
367
|
-
throw new Error(`${
|
|
368
|
-
if (
|
|
409
|
+
const f = await d.json();
|
|
410
|
+
if (typeof f.error == "string")
|
|
411
|
+
throw new Error(`${i} could not embed this URL: ${f.error}`);
|
|
412
|
+
if (a = W(i, l, f), !a.html)
|
|
369
413
|
throw new Error(
|
|
370
|
-
`${
|
|
414
|
+
`${i} returned no embed markup for this URL. Check the post is public, or paste the embed code instead.`
|
|
371
415
|
);
|
|
372
416
|
}
|
|
373
|
-
return
|
|
417
|
+
return p && m > 0 && r().set(b, a, m), a;
|
|
374
418
|
}
|
|
375
419
|
};
|
|
376
|
-
},
|
|
377
|
-
service:
|
|
378
|
-
oembed:
|
|
379
|
-
},
|
|
420
|
+
}, N = {
|
|
421
|
+
service: A,
|
|
422
|
+
oembed: F
|
|
423
|
+
}, G = {
|
|
380
424
|
register: y,
|
|
381
|
-
bootstrap:
|
|
382
|
-
destroy:
|
|
383
|
-
config:
|
|
425
|
+
bootstrap: k,
|
|
426
|
+
destroy: w,
|
|
427
|
+
config: C,
|
|
384
428
|
controllers: T,
|
|
385
|
-
routes:
|
|
386
|
-
services:
|
|
387
|
-
contentTypes:
|
|
388
|
-
policies:
|
|
389
|
-
middlewares:
|
|
429
|
+
routes: z,
|
|
430
|
+
services: N,
|
|
431
|
+
contentTypes: v,
|
|
432
|
+
policies: _,
|
|
433
|
+
middlewares: R
|
|
390
434
|
};
|
|
391
435
|
export {
|
|
392
|
-
|
|
436
|
+
G as default
|
|
393
437
|
};
|
|
@@ -4,7 +4,7 @@ declare const oembed: ({ strapi }: {
|
|
|
4
4
|
}) => {
|
|
5
5
|
/**
|
|
6
6
|
* GET /better-blocks/oembed?url=...&platform=...
|
|
7
|
-
* Proxies the platform oEmbed request server-side and returns the
|
|
7
|
+
* Proxies the platform oEmbed request server-side and returns the normalized
|
|
8
8
|
* payload used by the social-embed block.
|
|
9
9
|
*/
|
|
10
10
|
fetch(ctx: any): Promise<any>;
|
|
@@ -51,6 +51,7 @@ declare const _default: {
|
|
|
51
51
|
platforms: string[];
|
|
52
52
|
cache: boolean;
|
|
53
53
|
cacheTTL: number;
|
|
54
|
+
cacheMaxEntries: number;
|
|
54
55
|
instagram: {
|
|
55
56
|
accessToken: any;
|
|
56
57
|
};
|
|
@@ -115,6 +116,7 @@ declare const _default: {
|
|
|
115
116
|
strapi: import("@strapi/types/dist/core").Strapi;
|
|
116
117
|
}) => {
|
|
117
118
|
detectPlatform: (url: string) => import("./services/oembed").SocialPlatform;
|
|
119
|
+
clearCache: () => void;
|
|
118
120
|
fetch(url: string, platform?: import("./services/oembed").SocialPlatform): Promise<import("./services/oembed").NormalisedOEmbed>;
|
|
119
121
|
};
|
|
120
122
|
};
|
|
@@ -8,6 +8,7 @@ declare const _default: {
|
|
|
8
8
|
strapi: import("@strapi/types/dist/core").Strapi;
|
|
9
9
|
}) => {
|
|
10
10
|
detectPlatform: (url: string) => import("./oembed").SocialPlatform;
|
|
11
|
+
clearCache: () => void;
|
|
11
12
|
fetch(url: string, platform?: import("./oembed").SocialPlatform): Promise<import("./oembed").NormalisedOEmbed>;
|
|
12
13
|
};
|
|
13
14
|
};
|
|
@@ -12,13 +12,56 @@ export interface NormalisedOEmbed {
|
|
|
12
12
|
width?: number;
|
|
13
13
|
height?: number;
|
|
14
14
|
}
|
|
15
|
+
export interface SocialConfig {
|
|
16
|
+
enabled?: boolean;
|
|
17
|
+
platforms?: SocialPlatform[];
|
|
18
|
+
cache?: boolean;
|
|
19
|
+
cacheTTL?: number;
|
|
20
|
+
cacheMaxEntries?: number;
|
|
21
|
+
instagram?: {
|
|
22
|
+
accessToken?: string;
|
|
23
|
+
};
|
|
24
|
+
facebook?: {
|
|
25
|
+
accessToken?: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
15
28
|
export declare const detectPlatform: (url: string) => SocialPlatform | null;
|
|
29
|
+
/**
|
|
30
|
+
* Returns the platform oEmbed endpoint to call, or null when the platform has
|
|
31
|
+
* no public oEmbed API (e.g. LinkedIn, which we render via a constructed iframe).
|
|
32
|
+
*/
|
|
33
|
+
export declare const buildEndpoint: (platform: SocialPlatform, url: string, config: SocialConfig) => string | null;
|
|
34
|
+
/**
|
|
35
|
+
* `pin.it` short links are 30x redirects that Pinterest's oEmbed endpoint
|
|
36
|
+
* rejects outright, so expand them to the canonical `/pin/<id>/` URL first.
|
|
37
|
+
* Returns the original URL when it isn't a short link or can't be resolved.
|
|
38
|
+
*/
|
|
39
|
+
export declare const resolveShortUrl: (url: string) => Promise<string>;
|
|
40
|
+
/** LinkedIn has no oEmbed API but exposes a stable iframe embed by URN. */
|
|
41
|
+
export declare const buildLinkedInIframe: (url: string) => NormalisedOEmbed | null;
|
|
42
|
+
/**
|
|
43
|
+
* Platform oEmbed markup often ships the widget script inline (TikTok always
|
|
44
|
+
* does; Twitter/Instagram only when `omit_script` isn't honoured). Renderers
|
|
45
|
+
* inject that script themselves, and a `<script>` added through `innerHTML`
|
|
46
|
+
* never executes anyway — worse, its inert tag makes a renderer believe the
|
|
47
|
+
* widget is already loaded. Strip it so the stored html is markup only.
|
|
48
|
+
*/
|
|
49
|
+
export declare const stripScripts: (html: string) => string;
|
|
50
|
+
export declare const normalize: (platform: SocialPlatform, url: string, raw: Record<string, unknown>) => NormalisedOEmbed;
|
|
51
|
+
/**
|
|
52
|
+
* Bounded so a long-running instance cannot accumulate one entry per social post
|
|
53
|
+
* ever embedded. 500 posts is far more than an editor touches in a TTL window,
|
|
54
|
+
* and `cacheMaxEntries` raises it for anyone who disagrees.
|
|
55
|
+
*/
|
|
56
|
+
export declare const DEFAULT_CACHE_MAX_ENTRIES = 500;
|
|
16
57
|
declare const service: ({ strapi }: {
|
|
17
58
|
strapi: Core.Strapi;
|
|
18
59
|
}) => {
|
|
19
60
|
detectPlatform: (url: string) => SocialPlatform | null;
|
|
61
|
+
/** Exposed for tests and for anyone who needs to drop cached embeds. */
|
|
62
|
+
clearCache: () => void;
|
|
20
63
|
/**
|
|
21
|
-
* Fetch (and cache) the
|
|
64
|
+
* Fetch (and cache) the normalized oEmbed for `url`. `platform` may be
|
|
22
65
|
* passed to skip auto-detection. Throws on an unsupported/unconfigured
|
|
23
66
|
* platform or a failed upstream request.
|
|
24
67
|
*/
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A bounded cache with per-entry expiry.
|
|
3
|
+
*
|
|
4
|
+
* The oEmbed service used a bare `Map` keyed by URL, which meant one entry for
|
|
5
|
+
* every social post ever embedded, for the life of the process. Entries expired
|
|
6
|
+
* logically — a stale hit was discarded on read — but nothing ever removed them,
|
|
7
|
+
* so a long-running instance kept the memory whether or not anyone asked for
|
|
8
|
+
* that post again.
|
|
9
|
+
*
|
|
10
|
+
* Eviction is least-recently-used: a hit moves its entry to the end, and a write
|
|
11
|
+
* past `max` drops the entry at the front. `Map` iterates in insertion order and
|
|
12
|
+
* re-inserting moves a key to the end, which is the whole implementation.
|
|
13
|
+
*/
|
|
14
|
+
export interface TtlCache<T> {
|
|
15
|
+
get(key: string): T | undefined;
|
|
16
|
+
set(key: string, value: T, ttlMs: number): void;
|
|
17
|
+
delete(key: string): boolean;
|
|
18
|
+
clear(): void;
|
|
19
|
+
readonly size: number;
|
|
20
|
+
}
|
|
21
|
+
export declare const createTtlCache: <T>({ max }: {
|
|
22
|
+
max: number;
|
|
23
|
+
}) => TtlCache<T>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qkix/strapi-plugin-better-blocks",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.2",
|
|
4
4
|
"description": "An enhanced Rich Text (Blocks) field for Strapi with inline color picker and more",
|
|
5
5
|
"strapi": {
|
|
6
6
|
"kind": "plugin",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"remark-parse": "^11.0.0",
|
|
56
56
|
"shiki": "^4.3.0",
|
|
57
57
|
"unified": "^11.0.5",
|
|
58
|
-
"@qkix/better-blocks-core": "^0.2.
|
|
58
|
+
"@qkix/better-blocks-core": "^0.2.2"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@strapi/sdk-plugin": "^5.3.2",
|
|
@@ -73,8 +73,9 @@
|
|
|
73
73
|
"styled-components": "^6.1.17",
|
|
74
74
|
"typescript": "^5.8.3",
|
|
75
75
|
"vitest": "^3.2.4",
|
|
76
|
-
"@qkix/
|
|
77
|
-
"@qkix/
|
|
76
|
+
"@qkix/eslint-config": "0.0.0",
|
|
77
|
+
"@qkix/strapi-test-harness": "0.0.0",
|
|
78
|
+
"@qkix/tsconfig": "0.0.0"
|
|
78
79
|
},
|
|
79
80
|
"peerDependencies": {
|
|
80
81
|
"@radix-ui/react-toolbar": "^1.0.0",
|
|
@@ -110,7 +111,15 @@
|
|
|
110
111
|
"nx": {
|
|
111
112
|
"tags": [
|
|
112
113
|
"scope:plugin"
|
|
113
|
-
]
|
|
114
|
+
],
|
|
115
|
+
"targets": {
|
|
116
|
+
"test": {
|
|
117
|
+
"dependsOn": [
|
|
118
|
+
"build",
|
|
119
|
+
"^build"
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
}
|
|
114
123
|
},
|
|
115
124
|
"publishConfig": {
|
|
116
125
|
"access": "public"
|
|
@@ -127,6 +136,6 @@
|
|
|
127
136
|
"verify": "strapi-plugin verify",
|
|
128
137
|
"watch": "strapi-plugin watch",
|
|
129
138
|
"watch:link": "strapi-plugin watch:link",
|
|
130
|
-
"typecheck": "tsc -p admin/tsconfig.json && tsc -p server/tsconfig.json"
|
|
139
|
+
"typecheck": "tsc -p admin/tsconfig.json --noEmit && tsc -p server/tsconfig.json --noEmit"
|
|
131
140
|
}
|
|
132
141
|
}
|