@saastro/studio 0.3.3 → 0.4.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/dist/{chunk-M5BCEVWK.js → chunk-5EXFBVC2.js} +35 -9
- package/dist/{chunk-M5BCEVWK.js.map → chunk-5EXFBVC2.js.map} +1 -1
- package/dist/chunk-FLFH5RVH.js +10 -0
- package/dist/chunk-FLFH5RVH.js.map +1 -0
- package/dist/{chunk-NICLSTZK.js → chunk-IPVMSMB4.js} +3 -3
- package/dist/{chunk-NICLSTZK.js.map → chunk-IPVMSMB4.js.map} +1 -1
- package/dist/{chunk-7SQ7LA2J.js → chunk-S5K6H6MF.js} +2 -2
- package/dist/editable-image-B4UdcCKi.d.ts +13 -0
- package/dist/image/index.d.ts +78 -0
- package/dist/image/index.js +75 -0
- package/dist/image/index.js.map +1 -0
- package/dist/index.d.ts +2 -13
- package/dist/index.js +6 -9
- package/dist/index.js.map +1 -1
- package/dist/integration/index.js +3 -3
- package/dist/middleware/index.js +1 -1
- package/dist/vite/index.js +2 -2
- package/package.json +21 -16
- package/src/Img.astro +124 -0
- /package/dist/{chunk-7SQ7LA2J.js.map → chunk-S5K6H6MF.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/helpers/editable-image.ts"],"sourcesContent":["import type { EditableAttrs } from './types.js'\n\n/**\n * Image marker — drop on `<img>` elements so Studio can offer an\n * image picker on click without leaving the preview.\n *\n * Returns empty object when fieldPrefix is falsy — safe to spread unconditionally.\n */\nexport function editableImage(fieldPrefix: string | undefined): EditableAttrs {\n if (!fieldPrefix) return {}\n return { 'data-saastro': `img:${fieldPrefix}` }\n}\n"],"mappings":";AAQO,SAAS,cAAc,aAAgD;AAC5E,MAAI,CAAC,YAAa,QAAO,CAAC;AAC1B,SAAO,EAAE,gBAAgB,OAAO,WAAW,GAAG;AAChD;","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/vite/meta-tag.ts
|
|
2
2
|
var STUDIO_PROTOCOL_VERSION = "2";
|
|
3
|
-
var PACKAGE_VERSION = "0.
|
|
3
|
+
var PACKAGE_VERSION = "0.4.0".length > 0 ? "0.4.0" : "0.0.0+unknown";
|
|
4
4
|
var META_TAGS = [
|
|
5
5
|
`<meta name="saastro:studio-version" content="${STUDIO_PROTOCOL_VERSION}">`,
|
|
6
6
|
`<meta name="saastro:studio-package" content="${PACKAGE_VERSION}">`
|
|
@@ -48,7 +48,7 @@ function buildVisibilityStyleTag(config) {
|
|
|
48
48
|
}).join("");
|
|
49
49
|
return `<style id="${STYLE_ID}">${rules}</style>`;
|
|
50
50
|
}
|
|
51
|
-
function injectVisibilityStyle(html,
|
|
51
|
+
function injectVisibilityStyle(html, _projectRoot = ".") {
|
|
52
52
|
if (html.includes(`id="${STYLE_ID}"`)) return html;
|
|
53
53
|
let tag = "";
|
|
54
54
|
try {
|
|
@@ -68,4 +68,4 @@ export {
|
|
|
68
68
|
buildVisibilityStyleTag,
|
|
69
69
|
injectVisibilityStyle
|
|
70
70
|
};
|
|
71
|
-
//# sourceMappingURL=chunk-
|
|
71
|
+
//# sourceMappingURL=chunk-IPVMSMB4.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/vite/meta-tag.ts","../src/visibility-css.ts"],"sourcesContent":["/**\n * Injects two meta tags into the HTML <head>:\n * - <meta name=\"saastro:studio-version\" content=\"2\"> — protocol version\n * - <meta name=\"saastro:studio-package\" content=\"X.Y.Z\"> — npm package version\n *\n * B8 — spec v5.3 §2 bumps the postMessage protocol number to \"2\". The\n * `studio-version` field reads as the PROTOCOL version (the contract Hub\n * relies on when wiring overlay <-> host messages), not the npm package\n * version of `@saastro/studio`. The package version still ships in a\n * separate `saastro:studio-package` tag for telemetry.\n *\n * The package version comes from a tsup `define` substitution\n * (`__SAASTRO_STUDIO_VERSION__`) injected at build time. The previous\n * implementation used `createRequire(import.meta.url)` to read\n * `package.json` at runtime, but tsup's chunked output places dist\n * files at `dist/chunk-XXX.js` and `../../package.json` resolves\n * OUTSIDE the package — the require always threw and the catch fell\n * back to a hardcoded `'0.1.0'`. Build-time substitution is bullet-\n * proof regardless of how tsup chunks the bundle.\n *\n * This is separate from the auto-wrap transform hook —\n * transformIndexHtml runs on rendered HTML output, not on .astro\n * source files.\n */\n\n/**\n * postMessage protocol version. Bumped to \"2\" in spec v5.3 to add\n * `studio:sections:enumerate` + `studio:select:section` exchanges.\n * Increment when adding/changing the iframe⇄host wire format.\n */\nconst STUDIO_PROTOCOL_VERSION = '2'\n\ndeclare const __SAASTRO_STUDIO_VERSION__: string | undefined\n\nconst PACKAGE_VERSION =\n typeof __SAASTRO_STUDIO_VERSION__ === 'string' && __SAASTRO_STUDIO_VERSION__.length > 0\n ? __SAASTRO_STUDIO_VERSION__\n : '0.0.0+unknown'\n\nconst META_TAGS = [\n `<meta name=\"saastro:studio-version\" content=\"${STUDIO_PROTOCOL_VERSION}\">`,\n `<meta name=\"saastro:studio-package\" content=\"${PACKAGE_VERSION}\">`,\n].join('\\n ')\n\n/**\n * Inject the two studio meta tags into the HTML head. Idempotent —\n * skips injection if the protocol-version tag is already present\n * (matches both the new shape and the legacy single-tag shape, so\n * customer sites with old metadata aren't double-tagged on rebuild).\n */\nexport function injectVersionMetaTag(html: string): string {\n if (html.includes('saastro:studio-version')) return html\n return html.replace(/<head([^>]*)>/i, `<head$1>\\n ${META_TAGS}`)\n}\n","/**\n * Read `studio.config.json` from a project root and build the\n * `<style>` block that hides sections flagged as `hidden`.\n *\n * The config shape is the one written by the Studio Hub at publish time:\n *\n * {\n * \"hidden\": {\n * \"<pageFile>\": [\"<fieldPrefix>\", ...]\n * }\n * }\n *\n * The CSS targets the `data-saastro=\"sec:<fieldPrefix>\"` markers the\n * auto-wrap plugin emits — so hiding is \"exactly the section root,\n * nothing more\". Selectors are flattened across all `pageFile` entries:\n * Studio's `fieldPrefix` naming convention is `<sectionName>.<pageName>`\n * which keeps them unique site-wide, so this loses no precision.\n *\n * Both the Vite `transformIndexHtml` hook (static-build sites) and the\n * Astro middleware (SSR routes) call into the same helpers so the\n * injected CSS is identical regardless of render mode.\n */\n\n// **CRITICAL — DO NOT add static node:fs / node:path imports here.**\n// This module is loaded by `@saastro/studio/middleware`, which runs\n// inside Cloudflare Workers at SSR boot. CF Workers does not support\n// `node:fs` even with the `nodejs_compat` flag (sync fs I/O is\n// fundamentally incompatible with the worker model) — a top-level\n// `import { readFileSync } from 'node:fs'` causes the module to fail\n// to load with `Error: No such module \"node:fs\"`, which breaks the\n// entire middleware chain and makes Astro return 500 on every SSR\n// route.\n//\n// The `node:fs` fallback (used by `astro dev` in Node) loads via\n// dynamic import inside `readVisibilityConfig`, wrapped in try/catch.\n// Workers never reaches that path because the build-time `define`\n// (`__SAASTRO_STUDIO_HIDDEN_TAG__`) short-circuits the readFileSync\n// call entirely.\n\nconst CONFIG_FILENAME = 'studio.config.json'\nconst STYLE_ID = 'saastro-studio-hidden'\n\n/**\n * Compile-time constant injected by the `@saastro/studio` Vite plugin\n * (see `config()` hook in `vite/index.ts`). esbuild substitutes the\n * identifier at customer-bundle time with a JSON string of the\n * full `<style>` tag (or an empty string when nothing is hidden), so\n * the middleware doesn't need filesystem access at runtime — a\n * hard requirement on Cloudflare Workers, where `node:fs` is a no-op.\n *\n * Declared as a free variable on the global scope so esbuild's\n * `define` substitution recognises it as a literal-replacement\n * target. When the plugin isn't installed, the identifier remains\n * undefined and the middleware falls back to the readFileSync path\n * (works in node-based deployments).\n */\ndeclare const __SAASTRO_STUDIO_HIDDEN_TAG__: string | undefined\n\ninterface StudioConfig {\n hidden?: Record<string, string[]>\n}\n\n/**\n * Read studio.config.json from `projectRoot`. Returns an empty config\n * if the file is missing or malformed — a publish that hasn't written\n * the config yet behaves like \"nothing hidden\".\n *\n * Defaulting `projectRoot` to `'.'` lets callers omit it when they\n * cannot easily determine the resolved Vite root — `node:path.resolve`\n * resolves relative to the current working directory, which is the\n * project root in every realistic deployment of the middleware.\n */\n/**\n * Build-time-only helper. Imports `node:fs` lazily so this module\n * stays loadable on CF Workers (where `node:fs` is not available).\n * The Vite plugin (`vite/index.ts`, runs in Node) calls this during\n * `config()` to inline the visibility tag as a `define` constant.\n * The runtime middleware (`middleware/index.ts`, runs in Workers)\n * never calls it — it reads the inlined define directly.\n *\n * Returns `{}` when the file isn't there, isn't readable, or when\n * `node:fs` simply isn't available (e.g. bundled into a non-Node\n * runtime that somehow reached this branch).\n */\nexport async function readVisibilityConfig(\n projectRoot: string = '.',\n): Promise<StudioConfig> {\n try {\n const [{ readFileSync }, { resolve }] = await Promise.all([\n import('node:fs'),\n import('node:path'),\n ])\n const raw = readFileSync(resolve(projectRoot, CONFIG_FILENAME), 'utf8')\n if (!raw.trim()) return {}\n const parsed = JSON.parse(raw) as unknown\n if (parsed && typeof parsed === 'object') return parsed as StudioConfig\n return {}\n } catch {\n return {}\n }\n}\n\n/**\n * Synchronous variant kept for back-compat with callers that can't\n * await (e.g. test fixtures, legacy plugin hooks). Same lazy node:fs\n * import via `require` — works in Node CJS contexts, returns `{}`\n * on CF Workers / pure-ESM environments where `require` is undefined.\n *\n * The dynamic `globalThis.require` lookup avoids esbuild bundling the\n * module reference, which means CF Workers never throws\n * \"No such module 'node:fs'\" at import time.\n */\nexport function readVisibilityConfigSync(projectRoot: string = '.'): StudioConfig {\n try {\n const req = (globalThis as { require?: (id: string) => unknown }).require\n if (typeof req !== 'function') return {}\n const fs = req('node:fs') as { readFileSync: (p: string, e: string) => string }\n const path = req('node:path') as { resolve: (...parts: string[]) => string }\n const raw = fs.readFileSync(path.resolve(projectRoot, CONFIG_FILENAME), 'utf8')\n if (!raw.trim()) return {}\n const parsed = JSON.parse(raw) as unknown\n if (parsed && typeof parsed === 'object') return parsed as StudioConfig\n return {}\n } catch {\n return {}\n }\n}\n\n/**\n * Collect every fieldPrefix flagged as hidden across all pageFile\n * entries. Deduplicates so a prefix that appears under multiple\n * pageFiles only emits one selector.\n */\nexport function collectHiddenFieldPrefixes(config: StudioConfig): string[] {\n const set = new Set<string>()\n for (const prefixes of Object.values(config.hidden ?? {})) {\n if (!Array.isArray(prefixes)) continue\n for (const p of prefixes) {\n if (typeof p === 'string' && p.length > 0) set.add(p)\n }\n }\n return Array.from(set).sort()\n}\n\n/**\n * Build the `<style>` block. Returns an empty string when there's\n * nothing to hide so callers can no-op without conditional branching.\n */\nexport function buildVisibilityStyleTag(config: StudioConfig): string {\n const prefixes = collectHiddenFieldPrefixes(config)\n if (prefixes.length === 0) return ''\n const rules = prefixes\n .map(fp => {\n // CSS attribute-selector grammar: only `\\` and `\"` need escaping\n // inside `[attr=\"...\"]`. fieldPrefix is conventionally\n // `[a-zA-Z0-9._-]+` so this is belt-and-braces.\n const safe = `sec:${fp}`.replace(/[\"\\\\]/g, '\\\\$&')\n return `[data-saastro=\"${safe}\"]{display:none !important}`\n })\n .join('')\n return `<style id=\"${STYLE_ID}\">${rules}</style>`\n}\n\n/**\n * Inject the visibility `<style>` into an HTML document's `<head>`.\n * Idempotent: a page that already carries a `${STYLE_ID}` style is\n * returned unchanged.\n */\nexport function injectVisibilityStyle(html: string, projectRoot: string = '.'): string {\n if (html.includes(`id=\"${STYLE_ID}\"`)) return html\n // Prefer the build-time-injected tag when the customer's Vite build\n // populated `__SAASTRO_STUDIO_HIDDEN_TAG__` via the plugin's `config`\n // hook. This is the only path that works on Cloudflare Workers, where\n // `node:fs` is a no-op. The readFileSync fallback handles node-based\n // deployments (or local `astro dev` before the plugin's define lands).\n let tag = ''\n try {\n if (typeof __SAASTRO_STUDIO_HIDDEN_TAG__ === 'string') {\n tag = __SAASTRO_STUDIO_HIDDEN_TAG__\n }\n } catch {\n // ReferenceError swallowed — identifier wasn't substituted.\n }\n // No `readFileSync` fallback in this runtime path — that branch\n // only worked on Node and breaks CF Workers (`node:fs` import\n // throws \"No such module\"). When the build-time define isn't\n // populated (e.g. someone uses the middleware without the Vite\n // plugin), the visibility CSS just isn't injected — graceful\n // degradation, the meta tag still works.\n if (!tag) return html\n return html.replace(/<head([^>]*)>/i, `<head$1>\\n ${tag}`)\n}\n"],"mappings":";AA8BA,IAAM,0BAA0B;AAIhC,IAAM,kBAC8C,QAA2B,SAAS,IAClF,UACA;AAEN,IAAM,YAAY;AAAA,EAChB,gDAAgD,uBAAuB;AAAA,EACvE,gDAAgD,eAAe;AACjE,EAAE,KAAK,MAAM;AAQN,SAAS,qBAAqB,MAAsB;AACzD,MAAI,KAAK,SAAS,wBAAwB,EAAG,QAAO;AACpD,SAAO,KAAK,QAAQ,kBAAkB;AAAA,IAAe,SAAS,EAAE;AAClE;;;ACdA,IAAM,kBAAkB;AACxB,IAAM,WAAW;AAwEV,SAAS,yBAAyB,cAAsB,KAAmB;AAChF,MAAI;AACF,UAAM,MAAO,WAAqD;AAClE,QAAI,OAAO,QAAQ,WAAY,QAAO,CAAC;AACvC,UAAM,KAAK,IAAI,SAAS;AACxB,UAAM,OAAO,IAAI,WAAW;AAC5B,UAAM,MAAM,GAAG,aAAa,KAAK,QAAQ,aAAa,eAAe,GAAG,MAAM;AAC9E,QAAI,CAAC,IAAI,KAAK,EAAG,QAAO,CAAC;AACzB,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,QAAI,UAAU,OAAO,WAAW,SAAU,QAAO;AACjD,WAAO,CAAC;AAAA,EACV,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAOO,SAAS,2BAA2B,QAAgC;AACzE,QAAM,MAAM,oBAAI,IAAY;AAC5B,aAAW,YAAY,OAAO,OAAO,OAAO,UAAU,CAAC,CAAC,GAAG;AACzD,QAAI,CAAC,MAAM,QAAQ,QAAQ,EAAG;AAC9B,eAAW,KAAK,UAAU;AACxB,UAAI,OAAO,MAAM,YAAY,EAAE,SAAS,EAAG,KAAI,IAAI,CAAC;AAAA,IACtD;AAAA,EACF;AACA,SAAO,MAAM,KAAK,GAAG,EAAE,KAAK;AAC9B;AAMO,SAAS,wBAAwB,QAA8B;AACpE,QAAM,WAAW,2BAA2B,MAAM;AAClD,MAAI,SAAS,WAAW,EAAG,QAAO;AAClC,QAAM,QAAQ,SACX,IAAI,QAAM;AAIT,UAAM,OAAO,OAAO,EAAE,GAAG,QAAQ,UAAU,MAAM;AACjD,WAAO,kBAAkB,IAAI;AAAA,EAC/B,CAAC,EACA,KAAK,EAAE;AACV,SAAO,cAAc,QAAQ,KAAK,KAAK;AACzC;AAOO,SAAS,sBAAsB,MAAc,cAAsB,KAAa;AACrF,MAAI,KAAK,SAAS,OAAO,QAAQ,GAAG,EAAG,QAAO;AAM9C,MAAI,MAAM;AACV,MAAI;AACF,QAAI,OAAO,kCAAkC,UAAU;AACrD,YAAM;AAAA,IACR;AAAA,EACF,QAAQ;AAAA,EAER;AAOA,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,KAAK,QAAQ,kBAAkB;AAAA,IAAe,GAAG,EAAE;AAC5D;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/vite/meta-tag.ts","../src/visibility-css.ts"],"sourcesContent":["/**\n * Injects two meta tags into the HTML <head>:\n * - <meta name=\"saastro:studio-version\" content=\"2\"> — protocol version\n * - <meta name=\"saastro:studio-package\" content=\"X.Y.Z\"> — npm package version\n *\n * B8 — spec v5.3 §2 bumps the postMessage protocol number to \"2\". The\n * `studio-version` field reads as the PROTOCOL version (the contract Hub\n * relies on when wiring overlay <-> host messages), not the npm package\n * version of `@saastro/studio`. The package version still ships in a\n * separate `saastro:studio-package` tag for telemetry.\n *\n * The package version comes from a tsup `define` substitution\n * (`__SAASTRO_STUDIO_VERSION__`) injected at build time. The previous\n * implementation used `createRequire(import.meta.url)` to read\n * `package.json` at runtime, but tsup's chunked output places dist\n * files at `dist/chunk-XXX.js` and `../../package.json` resolves\n * OUTSIDE the package — the require always threw and the catch fell\n * back to a hardcoded `'0.1.0'`. Build-time substitution is bullet-\n * proof regardless of how tsup chunks the bundle.\n *\n * This is separate from the auto-wrap transform hook —\n * transformIndexHtml runs on rendered HTML output, not on .astro\n * source files.\n */\n\n/**\n * postMessage protocol version. Bumped to \"2\" in spec v5.3 to add\n * `studio:sections:enumerate` + `studio:select:section` exchanges.\n * Increment when adding/changing the iframe⇄host wire format.\n */\nconst STUDIO_PROTOCOL_VERSION = '2'\n\ndeclare const __SAASTRO_STUDIO_VERSION__: string | undefined\n\nconst PACKAGE_VERSION =\n typeof __SAASTRO_STUDIO_VERSION__ === 'string' && __SAASTRO_STUDIO_VERSION__.length > 0\n ? __SAASTRO_STUDIO_VERSION__\n : '0.0.0+unknown'\n\nconst META_TAGS = [\n `<meta name=\"saastro:studio-version\" content=\"${STUDIO_PROTOCOL_VERSION}\">`,\n `<meta name=\"saastro:studio-package\" content=\"${PACKAGE_VERSION}\">`,\n].join('\\n ')\n\n/**\n * Inject the two studio meta tags into the HTML head. Idempotent —\n * skips injection if the protocol-version tag is already present\n * (matches both the new shape and the legacy single-tag shape, so\n * customer sites with old metadata aren't double-tagged on rebuild).\n */\nexport function injectVersionMetaTag(html: string): string {\n if (html.includes('saastro:studio-version')) return html\n return html.replace(/<head([^>]*)>/i, `<head$1>\\n ${META_TAGS}`)\n}\n","/**\n * Read `studio.config.json` from a project root and build the\n * `<style>` block that hides sections flagged as `hidden`.\n *\n * The config shape is the one written by the Studio Hub at publish time:\n *\n * {\n * \"hidden\": {\n * \"<pageFile>\": [\"<fieldPrefix>\", ...]\n * }\n * }\n *\n * The CSS targets the `data-saastro=\"sec:<fieldPrefix>\"` markers the\n * auto-wrap plugin emits — so hiding is \"exactly the section root,\n * nothing more\". Selectors are flattened across all `pageFile` entries:\n * Studio's `fieldPrefix` naming convention is `<sectionName>.<pageName>`\n * which keeps them unique site-wide, so this loses no precision.\n *\n * Both the Vite `transformIndexHtml` hook (static-build sites) and the\n * Astro middleware (SSR routes) call into the same helpers so the\n * injected CSS is identical regardless of render mode.\n */\n\n// **CRITICAL — DO NOT add static node:fs / node:path imports here.**\n// This module is loaded by `@saastro/studio/middleware`, which runs\n// inside Cloudflare Workers at SSR boot. CF Workers does not support\n// `node:fs` even with the `nodejs_compat` flag (sync fs I/O is\n// fundamentally incompatible with the worker model) — a top-level\n// `import { readFileSync } from 'node:fs'` causes the module to fail\n// to load with `Error: No such module \"node:fs\"`, which breaks the\n// entire middleware chain and makes Astro return 500 on every SSR\n// route.\n//\n// The `node:fs` fallback (used by `astro dev` in Node) loads via\n// dynamic import inside `readVisibilityConfig`, wrapped in try/catch.\n// Workers never reaches that path because the build-time `define`\n// (`__SAASTRO_STUDIO_HIDDEN_TAG__`) short-circuits the readFileSync\n// call entirely.\n\nconst CONFIG_FILENAME = 'studio.config.json'\nconst STYLE_ID = 'saastro-studio-hidden'\n\n/**\n * Compile-time constant injected by the `@saastro/studio` Vite plugin\n * (see `config()` hook in `vite/index.ts`). esbuild substitutes the\n * identifier at customer-bundle time with a JSON string of the\n * full `<style>` tag (or an empty string when nothing is hidden), so\n * the middleware doesn't need filesystem access at runtime — a\n * hard requirement on Cloudflare Workers, where `node:fs` is a no-op.\n *\n * Declared as a free variable on the global scope so esbuild's\n * `define` substitution recognises it as a literal-replacement\n * target. When the plugin isn't installed, the identifier remains\n * undefined and the middleware falls back to the readFileSync path\n * (works in node-based deployments).\n */\ndeclare const __SAASTRO_STUDIO_HIDDEN_TAG__: string | undefined\n\ninterface StudioConfig {\n hidden?: Record<string, string[]>\n}\n\n/**\n * Read studio.config.json from `projectRoot`. Returns an empty config\n * if the file is missing or malformed — a publish that hasn't written\n * the config yet behaves like \"nothing hidden\".\n *\n * Defaulting `projectRoot` to `'.'` lets callers omit it when they\n * cannot easily determine the resolved Vite root — `node:path.resolve`\n * resolves relative to the current working directory, which is the\n * project root in every realistic deployment of the middleware.\n */\n/**\n * Build-time-only helper. Imports `node:fs` lazily so this module\n * stays loadable on CF Workers (where `node:fs` is not available).\n * The Vite plugin (`vite/index.ts`, runs in Node) calls this during\n * `config()` to inline the visibility tag as a `define` constant.\n * The runtime middleware (`middleware/index.ts`, runs in Workers)\n * never calls it — it reads the inlined define directly.\n *\n * Returns `{}` when the file isn't there, isn't readable, or when\n * `node:fs` simply isn't available (e.g. bundled into a non-Node\n * runtime that somehow reached this branch).\n */\nexport async function readVisibilityConfig(\n projectRoot: string = '.',\n): Promise<StudioConfig> {\n try {\n const [{ readFileSync }, { resolve }] = await Promise.all([\n import('node:fs'),\n import('node:path'),\n ])\n const raw = readFileSync(resolve(projectRoot, CONFIG_FILENAME), 'utf8')\n if (!raw.trim()) return {}\n const parsed = JSON.parse(raw) as unknown\n if (parsed && typeof parsed === 'object') return parsed as StudioConfig\n return {}\n } catch {\n return {}\n }\n}\n\n/**\n * Synchronous variant kept for back-compat with callers that can't\n * await (e.g. test fixtures, legacy plugin hooks). Same lazy node:fs\n * import via `require` — works in Node CJS contexts, returns `{}`\n * on CF Workers / pure-ESM environments where `require` is undefined.\n *\n * The dynamic `globalThis.require` lookup avoids esbuild bundling the\n * module reference, which means CF Workers never throws\n * \"No such module 'node:fs'\" at import time.\n */\nexport function readVisibilityConfigSync(projectRoot: string = '.'): StudioConfig {\n try {\n const req = (globalThis as { require?: (id: string) => unknown }).require\n if (typeof req !== 'function') return {}\n const fs = req('node:fs') as { readFileSync: (p: string, e: string) => string }\n const path = req('node:path') as { resolve: (...parts: string[]) => string }\n const raw = fs.readFileSync(path.resolve(projectRoot, CONFIG_FILENAME), 'utf8')\n if (!raw.trim()) return {}\n const parsed = JSON.parse(raw) as unknown\n if (parsed && typeof parsed === 'object') return parsed as StudioConfig\n return {}\n } catch {\n return {}\n }\n}\n\n/**\n * Collect every fieldPrefix flagged as hidden across all pageFile\n * entries. Deduplicates so a prefix that appears under multiple\n * pageFiles only emits one selector.\n */\nexport function collectHiddenFieldPrefixes(config: StudioConfig): string[] {\n const set = new Set<string>()\n for (const prefixes of Object.values(config.hidden ?? {})) {\n if (!Array.isArray(prefixes)) continue\n for (const p of prefixes) {\n if (typeof p === 'string' && p.length > 0) set.add(p)\n }\n }\n return Array.from(set).sort()\n}\n\n/**\n * Build the `<style>` block. Returns an empty string when there's\n * nothing to hide so callers can no-op without conditional branching.\n */\nexport function buildVisibilityStyleTag(config: StudioConfig): string {\n const prefixes = collectHiddenFieldPrefixes(config)\n if (prefixes.length === 0) return ''\n const rules = prefixes\n .map(fp => {\n // CSS attribute-selector grammar: only `\\` and `\"` need escaping\n // inside `[attr=\"...\"]`. fieldPrefix is conventionally\n // `[a-zA-Z0-9._-]+` so this is belt-and-braces.\n const safe = `sec:${fp}`.replace(/[\"\\\\]/g, '\\\\$&')\n return `[data-saastro=\"${safe}\"]{display:none !important}`\n })\n .join('')\n return `<style id=\"${STYLE_ID}\">${rules}</style>`\n}\n\n/**\n * Inject the visibility `<style>` into an HTML document's `<head>`.\n * Idempotent: a page that already carries a `${STYLE_ID}` style is\n * returned unchanged.\n */\nexport function injectVisibilityStyle(html: string, _projectRoot: string = '.'): string {\n if (html.includes(`id=\"${STYLE_ID}\"`)) return html\n // Prefer the build-time-injected tag when the customer's Vite build\n // populated `__SAASTRO_STUDIO_HIDDEN_TAG__` via the plugin's `config`\n // hook. This is the only path that works on Cloudflare Workers, where\n // `node:fs` is a no-op. The readFileSync fallback handles node-based\n // deployments (or local `astro dev` before the plugin's define lands).\n let tag = ''\n try {\n if (typeof __SAASTRO_STUDIO_HIDDEN_TAG__ === 'string') {\n tag = __SAASTRO_STUDIO_HIDDEN_TAG__\n }\n } catch {\n // ReferenceError swallowed — identifier wasn't substituted.\n }\n // No `readFileSync` fallback in this runtime path — that branch\n // only worked on Node and breaks CF Workers (`node:fs` import\n // throws \"No such module\"). When the build-time define isn't\n // populated (e.g. someone uses the middleware without the Vite\n // plugin), the visibility CSS just isn't injected — graceful\n // degradation, the meta tag still works.\n if (!tag) return html\n return html.replace(/<head([^>]*)>/i, `<head$1>\\n ${tag}`)\n}\n"],"mappings":";AA8BA,IAAM,0BAA0B;AAIhC,IAAM,kBAC8C,QAA2B,SAAS,IAClF,UACA;AAEN,IAAM,YAAY;AAAA,EAChB,gDAAgD,uBAAuB;AAAA,EACvE,gDAAgD,eAAe;AACjE,EAAE,KAAK,MAAM;AAQN,SAAS,qBAAqB,MAAsB;AACzD,MAAI,KAAK,SAAS,wBAAwB,EAAG,QAAO;AACpD,SAAO,KAAK,QAAQ,kBAAkB;AAAA,IAAe,SAAS,EAAE;AAClE;;;ACdA,IAAM,kBAAkB;AACxB,IAAM,WAAW;AAwEV,SAAS,yBAAyB,cAAsB,KAAmB;AAChF,MAAI;AACF,UAAM,MAAO,WAAqD;AAClE,QAAI,OAAO,QAAQ,WAAY,QAAO,CAAC;AACvC,UAAM,KAAK,IAAI,SAAS;AACxB,UAAM,OAAO,IAAI,WAAW;AAC5B,UAAM,MAAM,GAAG,aAAa,KAAK,QAAQ,aAAa,eAAe,GAAG,MAAM;AAC9E,QAAI,CAAC,IAAI,KAAK,EAAG,QAAO,CAAC;AACzB,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,QAAI,UAAU,OAAO,WAAW,SAAU,QAAO;AACjD,WAAO,CAAC;AAAA,EACV,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAOO,SAAS,2BAA2B,QAAgC;AACzE,QAAM,MAAM,oBAAI,IAAY;AAC5B,aAAW,YAAY,OAAO,OAAO,OAAO,UAAU,CAAC,CAAC,GAAG;AACzD,QAAI,CAAC,MAAM,QAAQ,QAAQ,EAAG;AAC9B,eAAW,KAAK,UAAU;AACxB,UAAI,OAAO,MAAM,YAAY,EAAE,SAAS,EAAG,KAAI,IAAI,CAAC;AAAA,IACtD;AAAA,EACF;AACA,SAAO,MAAM,KAAK,GAAG,EAAE,KAAK;AAC9B;AAMO,SAAS,wBAAwB,QAA8B;AACpE,QAAM,WAAW,2BAA2B,MAAM;AAClD,MAAI,SAAS,WAAW,EAAG,QAAO;AAClC,QAAM,QAAQ,SACX,IAAI,QAAM;AAIT,UAAM,OAAO,OAAO,EAAE,GAAG,QAAQ,UAAU,MAAM;AACjD,WAAO,kBAAkB,IAAI;AAAA,EAC/B,CAAC,EACA,KAAK,EAAE;AACV,SAAO,cAAc,QAAQ,KAAK,KAAK;AACzC;AAOO,SAAS,sBAAsB,MAAc,eAAuB,KAAa;AACtF,MAAI,KAAK,SAAS,OAAO,QAAQ,GAAG,EAAG,QAAO;AAM9C,MAAI,MAAM;AACV,MAAI;AACF,QAAI,OAAO,kCAAkC,UAAU;AACrD,YAAM;AAAA,IACR;AAAA,EACF,QAAQ;AAAA,EAER;AAOA,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,KAAK,QAAQ,kBAAkB;AAAA,IAAe,GAAG,EAAE;AAC5D;","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
studioAutoWrap
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-5EXFBVC2.js";
|
|
4
4
|
|
|
5
5
|
// src/integration/index.ts
|
|
6
6
|
function saastro(options = {}) {
|
|
@@ -25,4 +25,4 @@ function saastro(options = {}) {
|
|
|
25
25
|
export {
|
|
26
26
|
saastro
|
|
27
27
|
};
|
|
28
|
-
//# sourceMappingURL=chunk-
|
|
28
|
+
//# sourceMappingURL=chunk-S5K6H6MF.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface EditableAttrs {
|
|
2
|
+
[key: string]: string;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Image marker — drop on `<img>` elements so Studio can offer an
|
|
7
|
+
* image picker on click without leaving the preview.
|
|
8
|
+
*
|
|
9
|
+
* Returns empty object when fieldPrefix is falsy — safe to spread unconditionally.
|
|
10
|
+
*/
|
|
11
|
+
declare function editableImage(fieldPrefix: string | undefined): EditableAttrs;
|
|
12
|
+
|
|
13
|
+
export { type EditableAttrs as E, editableImage as e };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export { e as editableImage } from '../editable-image-B4UdcCKi.js';
|
|
2
|
+
|
|
3
|
+
type SrcKind = 'repo' | 'r2' | 'remote';
|
|
4
|
+
interface CfTransformOpts {
|
|
5
|
+
width: number;
|
|
6
|
+
/** 1–100; default 80. */
|
|
7
|
+
quality?: number;
|
|
8
|
+
format?: 'auto';
|
|
9
|
+
fit?: 'cover' | 'contain' | 'scale-down';
|
|
10
|
+
}
|
|
11
|
+
interface BuildImageInput {
|
|
12
|
+
/** Original content string: `/images/...` (repo) or `https://media.saastro.io/...` (R2). */
|
|
13
|
+
src: string;
|
|
14
|
+
/** srcset breakpoints in px. */
|
|
15
|
+
widths: number[];
|
|
16
|
+
/** 1–100; default 80. */
|
|
17
|
+
quality?: number;
|
|
18
|
+
fit?: CfTransformOpts['fit'];
|
|
19
|
+
/** false ⇒ dev / no-CF-edge ⇒ emit the raw src (no transformation). */
|
|
20
|
+
optimize: boolean;
|
|
21
|
+
/** Override the media host (default `media.saastro.io`). */
|
|
22
|
+
mediaHost?: string;
|
|
23
|
+
}
|
|
24
|
+
interface BuildImageResult {
|
|
25
|
+
/** Element-level src — ALWAYS the original string (the Studio overlay reads this back). */
|
|
26
|
+
rawSrc: string;
|
|
27
|
+
/** Width-keyed transformed URLs for `<source srcset>`, or null when not optimizing/transformable. */
|
|
28
|
+
srcset: string | null;
|
|
29
|
+
/** Largest transformed URL — for the `<img>` src when the image is NOT editable. */
|
|
30
|
+
optimizedSrc: string | null;
|
|
31
|
+
kind: SrcKind;
|
|
32
|
+
}
|
|
33
|
+
interface CfImageUrlOpts {
|
|
34
|
+
/** Target width in px. */
|
|
35
|
+
width: number;
|
|
36
|
+
/** 1–100; default 80. */
|
|
37
|
+
quality?: number;
|
|
38
|
+
fit?: CfTransformOpts['fit'];
|
|
39
|
+
/** Override the media host (default `media.saastro.io`). */
|
|
40
|
+
mediaHost?: string;
|
|
41
|
+
/** false ⇒ return the raw src (dev / no CF edge). Default true. */
|
|
42
|
+
optimize?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Canonical Saastro media (R2 custom domain). Override per-tenant via `mediaHost`. */
|
|
46
|
+
declare const DEFAULT_MEDIA_HOST = "media.saastro.io";
|
|
47
|
+
declare const DEFAULT_WIDTHS: readonly number[];
|
|
48
|
+
declare const DEFAULT_QUALITY = 80;
|
|
49
|
+
/**
|
|
50
|
+
* Classify a content image string by where its bytes live:
|
|
51
|
+
* - 'repo' → a `public/` asset referenced root-relative (`/images/...`)
|
|
52
|
+
* - 'r2' → a Saastro media-library upload on the media host
|
|
53
|
+
* - 'remote' → any other absolute URL (third-party) → never transformed
|
|
54
|
+
*/
|
|
55
|
+
declare function classifySrc(src: string, mediaHost?: string): SrcKind;
|
|
56
|
+
declare function isTransformable(src: string): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Build the element-level `src` plus a transformed `srcset` for a content image.
|
|
59
|
+
* When `optimize` is false (dev / no CF edge) or the source isn't transformable
|
|
60
|
+
* (svg/gif/remote), returns the raw src with no srcset so the caller renders a
|
|
61
|
+
* plain `<img>` — identical to today's behavior, nothing breaks.
|
|
62
|
+
*/
|
|
63
|
+
declare function buildImage(input: BuildImageInput): BuildImageResult;
|
|
64
|
+
/**
|
|
65
|
+
* Single optimized URL for ONE image — for CSS backgrounds
|
|
66
|
+
* (`background-image: url(...)`), inline styles, `<meta>` og:image, etc. —
|
|
67
|
+
* anywhere a `srcset` doesn't apply. Returns the raw src unchanged for
|
|
68
|
+
* non-transformable sources (svg/gif/remote) or when `optimize` is false, so
|
|
69
|
+
* it's always safe to drop in.
|
|
70
|
+
*
|
|
71
|
+
* Dev-gating note: this is a PURE helper — it can't read `import.meta.env.DEV`
|
|
72
|
+
* (it runs from the compiled runtime, not the consumer's Vite context). In an
|
|
73
|
+
* `.astro` file, gate it yourself so local dev serves the raw bytes:
|
|
74
|
+
* const bg = cfImageUrl(src, { width: 1920, optimize: !import.meta.env.DEV })
|
|
75
|
+
*/
|
|
76
|
+
declare function cfImageUrl(src: string, opts: CfImageUrlOpts): string;
|
|
77
|
+
|
|
78
|
+
export { type BuildImageInput, type BuildImageResult, type CfImageUrlOpts, type CfTransformOpts, DEFAULT_MEDIA_HOST, DEFAULT_QUALITY, DEFAULT_WIDTHS, type SrcKind, buildImage, cfImageUrl, classifySrc, isTransformable };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
editableImage
|
|
3
|
+
} from "../chunk-FLFH5RVH.js";
|
|
4
|
+
|
|
5
|
+
// src/image/cf-image.ts
|
|
6
|
+
var DEFAULT_MEDIA_HOST = "media.saastro.io";
|
|
7
|
+
var DEFAULT_WIDTHS = [480, 768, 1024, 1536, 2048];
|
|
8
|
+
var DEFAULT_QUALITY = 80;
|
|
9
|
+
var NO_TRANSFORM_EXT = /\.(svg|gif|ico|avif)$/i;
|
|
10
|
+
function classifySrc(src, mediaHost = DEFAULT_MEDIA_HOST) {
|
|
11
|
+
if (src.startsWith("/")) return "repo";
|
|
12
|
+
try {
|
|
13
|
+
const u = new URL(src);
|
|
14
|
+
return u.host === mediaHost ? "r2" : "remote";
|
|
15
|
+
} catch {
|
|
16
|
+
return "repo";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function isTransformable(src) {
|
|
20
|
+
const path = src.split(/[?#]/)[0] ?? src;
|
|
21
|
+
return !NO_TRANSFORM_EXT.test(path);
|
|
22
|
+
}
|
|
23
|
+
function cfOptions(o) {
|
|
24
|
+
const parts = [
|
|
25
|
+
`width=${o.width}`,
|
|
26
|
+
`format=${o.format ?? "auto"}`,
|
|
27
|
+
`quality=${o.quality ?? DEFAULT_QUALITY}`
|
|
28
|
+
];
|
|
29
|
+
if (o.fit) parts.push(`fit=${o.fit}`);
|
|
30
|
+
return parts.join(",");
|
|
31
|
+
}
|
|
32
|
+
function cfRepoUrl(src, o) {
|
|
33
|
+
return `/cdn-cgi/image/${cfOptions(o)}${src}`;
|
|
34
|
+
}
|
|
35
|
+
function cfR2Url(src, o) {
|
|
36
|
+
const u = new URL(src);
|
|
37
|
+
return `https://${u.host}/cdn-cgi/image/${cfOptions(o)}${u.pathname}`;
|
|
38
|
+
}
|
|
39
|
+
function buildImage(input) {
|
|
40
|
+
const mediaHost = input.mediaHost ?? DEFAULT_MEDIA_HOST;
|
|
41
|
+
const kind = classifySrc(input.src, mediaHost);
|
|
42
|
+
const canTransform = input.optimize && kind !== "remote" && isTransformable(input.src);
|
|
43
|
+
if (!canTransform) {
|
|
44
|
+
return { rawSrc: input.src, srcset: null, optimizedSrc: null, kind };
|
|
45
|
+
}
|
|
46
|
+
const quality = input.quality ?? DEFAULT_QUALITY;
|
|
47
|
+
const widths = input.widths.length > 0 ? input.widths : [...DEFAULT_WIDTHS];
|
|
48
|
+
const toUrl = (w) => {
|
|
49
|
+
const o = { width: w, quality, fit: input.fit };
|
|
50
|
+
return kind === "r2" ? cfR2Url(input.src, o) : cfRepoUrl(input.src, o);
|
|
51
|
+
};
|
|
52
|
+
const srcset = widths.map((w) => `${toUrl(w)} ${w}w`).join(", ");
|
|
53
|
+
const largest = widths[widths.length - 1];
|
|
54
|
+
const optimizedSrc = toUrl(largest);
|
|
55
|
+
return { rawSrc: input.src, srcset, optimizedSrc, kind };
|
|
56
|
+
}
|
|
57
|
+
function cfImageUrl(src, opts) {
|
|
58
|
+
if (opts.optimize === false) return src;
|
|
59
|
+
const mediaHost = opts.mediaHost ?? DEFAULT_MEDIA_HOST;
|
|
60
|
+
const kind = classifySrc(src, mediaHost);
|
|
61
|
+
if (kind === "remote" || !isTransformable(src)) return src;
|
|
62
|
+
const o = { width: opts.width, quality: opts.quality, fit: opts.fit };
|
|
63
|
+
return kind === "r2" ? cfR2Url(src, o) : cfRepoUrl(src, o);
|
|
64
|
+
}
|
|
65
|
+
export {
|
|
66
|
+
DEFAULT_MEDIA_HOST,
|
|
67
|
+
DEFAULT_QUALITY,
|
|
68
|
+
DEFAULT_WIDTHS,
|
|
69
|
+
buildImage,
|
|
70
|
+
cfImageUrl,
|
|
71
|
+
classifySrc,
|
|
72
|
+
editableImage,
|
|
73
|
+
isTransformable
|
|
74
|
+
};
|
|
75
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/image/cf-image.ts"],"sourcesContent":["import type {\n SrcKind,\n CfTransformOpts,\n BuildImageInput,\n BuildImageResult,\n CfImageUrlOpts,\n} from './types.js'\n\n/** Canonical Saastro media (R2 custom domain). Override per-tenant via `mediaHost`. */\nexport const DEFAULT_MEDIA_HOST = 'media.saastro.io'\nexport const DEFAULT_WIDTHS: readonly number[] = [480, 768, 1024, 1536, 2048]\nexport const DEFAULT_QUALITY = 80\n\n/** Vector/animated/already-optimal formats CF can't meaningfully resize/recode → serve raw. */\nconst NO_TRANSFORM_EXT = /\\.(svg|gif|ico|avif)$/i\n\n/**\n * Classify a content image string by where its bytes live:\n * - 'repo' → a `public/` asset referenced root-relative (`/images/...`)\n * - 'r2' → a Saastro media-library upload on the media host\n * - 'remote' → any other absolute URL (third-party) → never transformed\n */\nexport function classifySrc(src: string, mediaHost: string = DEFAULT_MEDIA_HOST): SrcKind {\n if (src.startsWith('/')) return 'repo'\n try {\n const u = new URL(src)\n return u.host === mediaHost ? 'r2' : 'remote'\n } catch {\n // relative path without a leading slash → treat as a repo asset\n return 'repo'\n }\n}\n\nexport function isTransformable(src: string): boolean {\n const path = src.split(/[?#]/)[0] ?? src\n return !NO_TRANSFORM_EXT.test(path)\n}\n\nfunction cfOptions(o: CfTransformOpts): string {\n const parts = [\n `width=${o.width}`,\n `format=${o.format ?? 'auto'}`,\n `quality=${o.quality ?? DEFAULT_QUALITY}`,\n ]\n if (o.fit) parts.push(`fit=${o.fit}`)\n return parts.join(',')\n}\n\n/** Repo asset → root-relative; CF intercepts `/cdn-cgi/image/*` at the zone edge. */\nfunction cfRepoUrl(src: string, o: CfTransformOpts): string {\n return `/cdn-cgi/image/${cfOptions(o)}${src}`\n}\n\n/** R2 media → transform on the media host where the object + R2 binding live. */\nfunction cfR2Url(src: string, o: CfTransformOpts): string {\n const u = new URL(src)\n return `https://${u.host}/cdn-cgi/image/${cfOptions(o)}${u.pathname}`\n}\n\n/**\n * Build the element-level `src` plus a transformed `srcset` for a content image.\n * When `optimize` is false (dev / no CF edge) or the source isn't transformable\n * (svg/gif/remote), returns the raw src with no srcset so the caller renders a\n * plain `<img>` — identical to today's behavior, nothing breaks.\n */\nexport function buildImage(input: BuildImageInput): BuildImageResult {\n const mediaHost = input.mediaHost ?? DEFAULT_MEDIA_HOST\n const kind = classifySrc(input.src, mediaHost)\n const canTransform = input.optimize && kind !== 'remote' && isTransformable(input.src)\n\n if (!canTransform) {\n return { rawSrc: input.src, srcset: null, optimizedSrc: null, kind }\n }\n\n const quality = input.quality ?? DEFAULT_QUALITY\n const widths = input.widths.length > 0 ? input.widths : [...DEFAULT_WIDTHS]\n const toUrl = (w: number): string => {\n const o: CfTransformOpts = { width: w, quality, fit: input.fit }\n return kind === 'r2' ? cfR2Url(input.src, o) : cfRepoUrl(input.src, o)\n }\n\n const srcset = widths.map((w) => `${toUrl(w)} ${w}w`).join(', ')\n const largest = widths[widths.length - 1]!\n const optimizedSrc = toUrl(largest)\n return { rawSrc: input.src, srcset, optimizedSrc, kind }\n}\n\n/**\n * Single optimized URL for ONE image — for CSS backgrounds\n * (`background-image: url(...)`), inline styles, `<meta>` og:image, etc. —\n * anywhere a `srcset` doesn't apply. Returns the raw src unchanged for\n * non-transformable sources (svg/gif/remote) or when `optimize` is false, so\n * it's always safe to drop in.\n *\n * Dev-gating note: this is a PURE helper — it can't read `import.meta.env.DEV`\n * (it runs from the compiled runtime, not the consumer's Vite context). In an\n * `.astro` file, gate it yourself so local dev serves the raw bytes:\n * const bg = cfImageUrl(src, { width: 1920, optimize: !import.meta.env.DEV })\n */\nexport function cfImageUrl(src: string, opts: CfImageUrlOpts): string {\n if (opts.optimize === false) return src\n const mediaHost = opts.mediaHost ?? DEFAULT_MEDIA_HOST\n const kind = classifySrc(src, mediaHost)\n if (kind === 'remote' || !isTransformable(src)) return src\n const o: CfTransformOpts = { width: opts.width, quality: opts.quality, fit: opts.fit }\n return kind === 'r2' ? cfR2Url(src, o) : cfRepoUrl(src, o)\n}\n"],"mappings":";;;;;AASO,IAAM,qBAAqB;AAC3B,IAAM,iBAAoC,CAAC,KAAK,KAAK,MAAM,MAAM,IAAI;AACrE,IAAM,kBAAkB;AAG/B,IAAM,mBAAmB;AAQlB,SAAS,YAAY,KAAa,YAAoB,oBAA6B;AACxF,MAAI,IAAI,WAAW,GAAG,EAAG,QAAO;AAChC,MAAI;AACF,UAAM,IAAI,IAAI,IAAI,GAAG;AACrB,WAAO,EAAE,SAAS,YAAY,OAAO;AAAA,EACvC,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,gBAAgB,KAAsB;AACpD,QAAM,OAAO,IAAI,MAAM,MAAM,EAAE,CAAC,KAAK;AACrC,SAAO,CAAC,iBAAiB,KAAK,IAAI;AACpC;AAEA,SAAS,UAAU,GAA4B;AAC7C,QAAM,QAAQ;AAAA,IACZ,SAAS,EAAE,KAAK;AAAA,IAChB,UAAU,EAAE,UAAU,MAAM;AAAA,IAC5B,WAAW,EAAE,WAAW,eAAe;AAAA,EACzC;AACA,MAAI,EAAE,IAAK,OAAM,KAAK,OAAO,EAAE,GAAG,EAAE;AACpC,SAAO,MAAM,KAAK,GAAG;AACvB;AAGA,SAAS,UAAU,KAAa,GAA4B;AAC1D,SAAO,kBAAkB,UAAU,CAAC,CAAC,GAAG,GAAG;AAC7C;AAGA,SAAS,QAAQ,KAAa,GAA4B;AACxD,QAAM,IAAI,IAAI,IAAI,GAAG;AACrB,SAAO,WAAW,EAAE,IAAI,kBAAkB,UAAU,CAAC,CAAC,GAAG,EAAE,QAAQ;AACrE;AAQO,SAAS,WAAW,OAA0C;AACnE,QAAM,YAAY,MAAM,aAAa;AACrC,QAAM,OAAO,YAAY,MAAM,KAAK,SAAS;AAC7C,QAAM,eAAe,MAAM,YAAY,SAAS,YAAY,gBAAgB,MAAM,GAAG;AAErF,MAAI,CAAC,cAAc;AACjB,WAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,MAAM,cAAc,MAAM,KAAK;AAAA,EACrE;AAEA,QAAM,UAAU,MAAM,WAAW;AACjC,QAAM,SAAS,MAAM,OAAO,SAAS,IAAI,MAAM,SAAS,CAAC,GAAG,cAAc;AAC1E,QAAM,QAAQ,CAAC,MAAsB;AACnC,UAAM,IAAqB,EAAE,OAAO,GAAG,SAAS,KAAK,MAAM,IAAI;AAC/D,WAAO,SAAS,OAAO,QAAQ,MAAM,KAAK,CAAC,IAAI,UAAU,MAAM,KAAK,CAAC;AAAA,EACvE;AAEA,QAAM,SAAS,OAAO,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,IAAI;AAC/D,QAAM,UAAU,OAAO,OAAO,SAAS,CAAC;AACxC,QAAM,eAAe,MAAM,OAAO;AAClC,SAAO,EAAE,QAAQ,MAAM,KAAK,QAAQ,cAAc,KAAK;AACzD;AAcO,SAAS,WAAW,KAAa,MAA8B;AACpE,MAAI,KAAK,aAAa,MAAO,QAAO;AACpC,QAAM,YAAY,KAAK,aAAa;AACpC,QAAM,OAAO,YAAY,KAAK,SAAS;AACvC,MAAI,SAAS,YAAY,CAAC,gBAAgB,GAAG,EAAG,QAAO;AACvD,QAAM,IAAqB,EAAE,OAAO,KAAK,OAAO,SAAS,KAAK,SAAS,KAAK,KAAK,IAAI;AACrF,SAAO,SAAS,OAAO,QAAQ,KAAK,CAAC,IAAI,UAAU,KAAK,CAAC;AAC3D;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import saastro from './integration/index.js';
|
|
2
2
|
export { SaastroStudioOptions } from './integration/index.js';
|
|
3
|
+
import { E as EditableAttrs, e as editableImage } from './editable-image-B4UdcCKi.js';
|
|
3
4
|
import 'astro';
|
|
4
5
|
import './types-DlrT0PDc.js';
|
|
5
6
|
|
|
6
|
-
interface EditableAttrs {
|
|
7
|
-
[key: string]: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
7
|
/**
|
|
11
8
|
* Section marker — drop on the root element of a component that
|
|
12
9
|
* receives `fieldPrefix`. Lets the Studio overlay hover/select/drag
|
|
@@ -16,14 +13,6 @@ interface EditableAttrs {
|
|
|
16
13
|
*/
|
|
17
14
|
declare function editableSection(fieldPrefix: string | undefined): EditableAttrs;
|
|
18
15
|
|
|
19
|
-
/**
|
|
20
|
-
* Image marker — drop on `<img>` elements so Studio can offer an
|
|
21
|
-
* image picker on click without leaving the preview.
|
|
22
|
-
*
|
|
23
|
-
* Returns empty object when fieldPrefix is falsy — safe to spread unconditionally.
|
|
24
|
-
*/
|
|
25
|
-
declare function editableImage(fieldPrefix: string | undefined): EditableAttrs;
|
|
26
|
-
|
|
27
16
|
/**
|
|
28
17
|
* Slot marker — used for discrete named edits within a section
|
|
29
18
|
* (e.g. a CTA label inside a hero). `fieldPrefix` scopes the slot
|
|
@@ -74,4 +63,4 @@ declare const studioEditable: {
|
|
|
74
63
|
readonly arrayItem: typeof editableArrayItem;
|
|
75
64
|
};
|
|
76
65
|
|
|
77
|
-
export {
|
|
66
|
+
export { EditableAttrs, saastro as default, editableArrayItem, editableImage, editableSection, editableSlot, saastro, studioEditable };
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
saastro
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-S5K6H6MF.js";
|
|
4
|
+
import "./chunk-5EXFBVC2.js";
|
|
5
|
+
import "./chunk-IPVMSMB4.js";
|
|
6
|
+
import {
|
|
7
|
+
editableImage
|
|
8
|
+
} from "./chunk-FLFH5RVH.js";
|
|
6
9
|
|
|
7
10
|
// src/helpers/editable-section.ts
|
|
8
11
|
function editableSection(fieldPrefix) {
|
|
@@ -10,12 +13,6 @@ function editableSection(fieldPrefix) {
|
|
|
10
13
|
return { "data-saastro": `sec:${fieldPrefix}` };
|
|
11
14
|
}
|
|
12
15
|
|
|
13
|
-
// src/helpers/editable-image.ts
|
|
14
|
-
function editableImage(fieldPrefix) {
|
|
15
|
-
if (!fieldPrefix) return {};
|
|
16
|
-
return { "data-saastro": `img:${fieldPrefix}` };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
16
|
// src/helpers/editable-slot.ts
|
|
20
17
|
function editableSlot(fieldPrefix, slotName) {
|
|
21
18
|
if (!fieldPrefix || !slotName) return {};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/helpers/editable-section.ts","../src/helpers/editable-
|
|
1
|
+
{"version":3,"sources":["../src/helpers/editable-section.ts","../src/helpers/editable-slot.ts","../src/helpers/editable-array-item.ts","../src/index.ts"],"sourcesContent":["import type { EditableAttrs } from './types.js'\n\n/**\n * Section marker — drop on the root element of a component that\n * receives `fieldPrefix`. Lets the Studio overlay hover/select/drag\n * the whole section.\n *\n * Returns empty object when fieldPrefix is falsy — safe to spread unconditionally.\n */\nexport function editableSection(fieldPrefix: string | undefined): EditableAttrs {\n if (!fieldPrefix) return {}\n return { 'data-saastro': `sec:${fieldPrefix}` }\n}\n","import type { EditableAttrs } from './types.js'\n\n/**\n * Slot marker — used for discrete named edits within a section\n * (e.g. a CTA label inside a hero). `fieldPrefix` scopes the slot\n * to its parent component; `slotName` is the local identifier.\n *\n * Returns empty object when either argument is falsy — safe to spread unconditionally.\n */\nexport function editableSlot(\n fieldPrefix: string | undefined,\n slotName: string,\n): EditableAttrs {\n if (!fieldPrefix || !slotName) return {}\n return { 'data-saastro': `slot:${fieldPrefix}.${slotName}` }\n}\n","import type { EditableAttrs } from './types.js'\n\n/**\n * Array-item marker — used on each rendered item inside a `.map(...)`\n * over repeatable content (pricing plans, testimonials, feature cards).\n * Studio uses this to select/duplicate/reorder individual items.\n *\n * Format: `arr:{arrPath}[{index}]` — brackets follow JS/TS convention\n * (lodash, JSON Pointer) and avoid ambiguity with numeric literal keys.\n *\n * Returns empty object when fieldPrefix is falsy or index is negative/non-finite.\n */\nexport function editableArrayItem(\n fieldPrefix: string | undefined,\n index: number,\n): EditableAttrs {\n if (!fieldPrefix || index < 0 || !Number.isFinite(index)) return {}\n return { 'data-saastro': `arr:${fieldPrefix}[${index}]` }\n}\n","/**\n * @saastro/studio — build-time instrumentation for Saastro Studio.\n *\n * This package provides:\n * - **Astro Integration** (default export): `saastro({ autoWrap: true })` —\n * registers the Vite plugin through Astro's own pipeline so ordering against\n * the `.astro` compiler is deterministic on cold builds.\n * - **Editable helpers**: `editableSection`, `editableImage`, `editableSlot`,\n * `editableArrayItem` + the `studioEditable` namespace.\n * - **Vite plugin** (separate entry, back-compat): `@saastro/studio/vite`.\n *\n * NOTE: This package is the *runtime instrumentation* installed in Astro\n * sites. The Studio editor UI lives at hub.saastro.io and is NOT part of this\n * package.\n *\n * @saastro/studio and @saastro/cms are peer packages — neither depends on the\n * other.\n */\n\nimport saastro from './integration/index.js'\n\nexport default saastro\nexport { saastro }\nexport type { SaastroStudioOptions } from './integration/index.js'\n\nexport type { EditableAttrs } from './helpers/types.js'\nexport { editableSection } from './helpers/editable-section.js'\nexport { editableImage } from './helpers/editable-image.js'\nexport { editableSlot } from './helpers/editable-slot.js'\nexport { editableArrayItem } from './helpers/editable-array-item.js'\n\nimport { editableSection } from './helpers/editable-section.js'\nimport { editableImage } from './helpers/editable-image.js'\nimport { editableSlot } from './helpers/editable-slot.js'\nimport { editableArrayItem } from './helpers/editable-array-item.js'\n\n/**\n * Convenience namespace — all four helpers grouped under one object.\n */\nexport const studioEditable = {\n section: editableSection,\n image: editableImage,\n slot: editableSlot,\n arrayItem: editableArrayItem,\n} as const\n"],"mappings":";;;;;;;;;;AASO,SAAS,gBAAgB,aAAgD;AAC9E,MAAI,CAAC,YAAa,QAAO,CAAC;AAC1B,SAAO,EAAE,gBAAgB,OAAO,WAAW,GAAG;AAChD;;;ACHO,SAAS,aACd,aACA,UACe;AACf,MAAI,CAAC,eAAe,CAAC,SAAU,QAAO,CAAC;AACvC,SAAO,EAAE,gBAAgB,QAAQ,WAAW,IAAI,QAAQ,GAAG;AAC7D;;;ACHO,SAAS,kBACd,aACA,OACe;AACf,MAAI,CAAC,eAAe,QAAQ,KAAK,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO,CAAC;AAClE,SAAO,EAAE,gBAAgB,OAAO,WAAW,IAAI,KAAK,IAAI;AAC1D;;;ACGA,IAAO,cAAQ;AAkBR,IAAM,iBAAiB;AAAA,EAC5B,SAAS;AAAA,EACT,OAAO;AAAA,EACP,MAAM;AAAA,EACN,WAAW;AACb;","names":[]}
|
package/dist/middleware/index.js
CHANGED
package/dist/vite/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saastro/studio",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Build-time instrumentation for Saastro Studio
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Build-time instrumentation for Saastro Studio — Vite plugin + editable helpers for Astro sites managed by Saastro Hub",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "SAASTRO",
|
|
7
7
|
"keywords": [
|
|
@@ -37,20 +37,17 @@
|
|
|
37
37
|
"./middleware": {
|
|
38
38
|
"types": "./dist/middleware/index.d.ts",
|
|
39
39
|
"import": "./dist/middleware/index.js"
|
|
40
|
-
}
|
|
40
|
+
},
|
|
41
|
+
"./image": {
|
|
42
|
+
"types": "./dist/image/index.d.ts",
|
|
43
|
+
"import": "./dist/image/index.js"
|
|
44
|
+
},
|
|
45
|
+
"./Img.astro": "./src/Img.astro"
|
|
41
46
|
},
|
|
42
47
|
"files": [
|
|
43
|
-
"dist"
|
|
48
|
+
"dist",
|
|
49
|
+
"src/Img.astro"
|
|
44
50
|
],
|
|
45
|
-
"scripts": {
|
|
46
|
-
"build": "tsup",
|
|
47
|
-
"dev": "tsup --watch",
|
|
48
|
-
"typecheck": "tsc --noEmit",
|
|
49
|
-
"test": "vitest run",
|
|
50
|
-
"test:watch": "vitest",
|
|
51
|
-
"clean": "rm -rf dist",
|
|
52
|
-
"prepublishOnly": "pnpm run build"
|
|
53
|
-
},
|
|
54
51
|
"peerDependencies": {
|
|
55
52
|
"astro": "^5.0.0 || ^6.0.0",
|
|
56
53
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
|
|
@@ -64,10 +61,18 @@
|
|
|
64
61
|
"ts-morph": "^25.0.1"
|
|
65
62
|
},
|
|
66
63
|
"devDependencies": {
|
|
67
|
-
"@saastro/studio-core": "workspace:*",
|
|
68
64
|
"tsup": "^8.5.1",
|
|
69
65
|
"typescript": "^5.8.3",
|
|
70
66
|
"vite": "^7.0.0",
|
|
71
|
-
"vitest": "^3.2.4"
|
|
67
|
+
"vitest": "^3.2.4",
|
|
68
|
+
"@saastro/studio-core": "0.0.1"
|
|
69
|
+
},
|
|
70
|
+
"scripts": {
|
|
71
|
+
"build": "tsup",
|
|
72
|
+
"dev": "tsup --watch",
|
|
73
|
+
"typecheck": "tsc --noEmit",
|
|
74
|
+
"test": "vitest run",
|
|
75
|
+
"test:watch": "vitest",
|
|
76
|
+
"clean": "rm -rf dist"
|
|
72
77
|
}
|
|
73
|
-
}
|
|
78
|
+
}
|
package/src/Img.astro
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <Img> — edge-optimized, Studio-editable image for Saastro CMS sites.
|
|
4
|
+
*
|
|
5
|
+
* - Optimizes via Cloudflare Image Transformations (`/cdn-cgi/image/...`) at the
|
|
6
|
+
* zone edge — webp/avif + responsive srcset, no Sharp (works on SSR/Workers).
|
|
7
|
+
* - Handles BOTH a repo asset (`/images/...`, transformed root-relative on the
|
|
8
|
+
* site's own zone) AND an R2 media-library upload (`https://media.saastro.io/...`,
|
|
9
|
+
* transformed on the media host).
|
|
10
|
+
* - Stays EDITABLE: when `fieldPrefix` is set, the `<img>` carries the RAW src +
|
|
11
|
+
* the Studio marker (`editableImage`), while the optimized candidates live on
|
|
12
|
+
* `<source srcset>`. The overlay reads/writes the raw `<img src>` → round-trips
|
|
13
|
+
* cleanly into content (it must never see a `/cdn-cgi/image/...` URL).
|
|
14
|
+
* - Degrades gracefully: in `astro dev`, on localhost, with `raw`, or for
|
|
15
|
+
* svg/gif/remote sources, emits a plain `<img>` (identical to today).
|
|
16
|
+
*
|
|
17
|
+
* Ships with @saastro/studio (consumed via the `./Img.astro` export). The marker
|
|
18
|
+
* helper `editableImage` is native to this package; the raw-src requirement is
|
|
19
|
+
* dictated by the Hub overlay (apps/hub-react/src/server/studio/overlay-script.ts).
|
|
20
|
+
*/
|
|
21
|
+
import { buildImage, editableImage, DEFAULT_WIDTHS } from '@saastro/studio/image'
|
|
22
|
+
|
|
23
|
+
interface Props {
|
|
24
|
+
/** `/images/...` (repo) or `https://media.saastro.io/...` (R2). */
|
|
25
|
+
src: string
|
|
26
|
+
alt: string
|
|
27
|
+
width?: number
|
|
28
|
+
height?: number
|
|
29
|
+
/** Responsive `sizes`; default `100vw`. */
|
|
30
|
+
sizes?: string
|
|
31
|
+
/** srcset breakpoints (px); default [480,768,1024,1536,2048]. */
|
|
32
|
+
widths?: number[]
|
|
33
|
+
quality?: number
|
|
34
|
+
fit?: 'cover' | 'contain' | 'scale-down'
|
|
35
|
+
loading?: 'lazy' | 'eager'
|
|
36
|
+
fetchpriority?: 'high' | 'low' | 'auto'
|
|
37
|
+
decoding?: 'async' | 'sync' | 'auto'
|
|
38
|
+
/** Studio editability: emits `data-saastro="img:<fieldPrefix>"`. */
|
|
39
|
+
fieldPrefix?: string
|
|
40
|
+
class?: string
|
|
41
|
+
/** Force-disable CF transforms for this instance (serve the raw src). */
|
|
42
|
+
raw?: boolean
|
|
43
|
+
/** Override the media host (default `media.saastro.io`). */
|
|
44
|
+
mediaHost?: string
|
|
45
|
+
[key: string]: unknown
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const {
|
|
49
|
+
src,
|
|
50
|
+
alt,
|
|
51
|
+
width,
|
|
52
|
+
height,
|
|
53
|
+
sizes = '100vw',
|
|
54
|
+
widths,
|
|
55
|
+
quality = 80,
|
|
56
|
+
fit,
|
|
57
|
+
loading = 'lazy',
|
|
58
|
+
fetchpriority,
|
|
59
|
+
decoding = 'async',
|
|
60
|
+
fieldPrefix,
|
|
61
|
+
class: className,
|
|
62
|
+
raw = false,
|
|
63
|
+
mediaHost,
|
|
64
|
+
...rest
|
|
65
|
+
} = Astro.props
|
|
66
|
+
|
|
67
|
+
// Only emit /cdn-cgi/image URLs where a CF edge will actually serve them.
|
|
68
|
+
// `import.meta.env.DEV` is statically true in `astro dev` (no edge there);
|
|
69
|
+
// the host check is a belt-and-suspenders guard for `astro preview` /
|
|
70
|
+
// `wrangler dev`; PUBLIC_SAASTRO_IMG_RAW is a per-environment kill switch.
|
|
71
|
+
const isDev = import.meta.env.DEV === true
|
|
72
|
+
const reqHost = Astro.request.headers.get('host') ?? ''
|
|
73
|
+
const isLocalHost = /^(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/.test(reqHost)
|
|
74
|
+
const forceRaw = import.meta.env.PUBLIC_SAASTRO_IMG_RAW === '1'
|
|
75
|
+
const optimize = !isDev && !isLocalHost && !forceRaw && !raw
|
|
76
|
+
|
|
77
|
+
const img = buildImage({
|
|
78
|
+
src,
|
|
79
|
+
widths: widths ?? [...DEFAULT_WIDTHS],
|
|
80
|
+
quality,
|
|
81
|
+
fit,
|
|
82
|
+
optimize,
|
|
83
|
+
mediaHost,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const editAttrs = editableImage(fieldPrefix)
|
|
87
|
+
const isEditable = !!fieldPrefix
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
{
|
|
91
|
+
img.srcset ? (
|
|
92
|
+
<picture>
|
|
93
|
+
{/* One <source>: `format=auto` already negotiates avif/webp/jpeg at the
|
|
94
|
+
edge, so a single transformed srcset is enough. */}
|
|
95
|
+
<source srcset={img.srcset} sizes={sizes} />
|
|
96
|
+
<img
|
|
97
|
+
src={isEditable ? img.rawSrc : (img.optimizedSrc ?? img.rawSrc)}
|
|
98
|
+
alt={alt}
|
|
99
|
+
width={width}
|
|
100
|
+
height={height}
|
|
101
|
+
sizes={sizes}
|
|
102
|
+
loading={loading}
|
|
103
|
+
fetchpriority={fetchpriority}
|
|
104
|
+
decoding={decoding}
|
|
105
|
+
class={className}
|
|
106
|
+
{...editAttrs}
|
|
107
|
+
{...rest}
|
|
108
|
+
/>
|
|
109
|
+
</picture>
|
|
110
|
+
) : (
|
|
111
|
+
<img
|
|
112
|
+
src={img.rawSrc}
|
|
113
|
+
alt={alt}
|
|
114
|
+
width={width}
|
|
115
|
+
height={height}
|
|
116
|
+
loading={loading}
|
|
117
|
+
fetchpriority={fetchpriority}
|
|
118
|
+
decoding={decoding}
|
|
119
|
+
class={className}
|
|
120
|
+
{...editAttrs}
|
|
121
|
+
{...rest}
|
|
122
|
+
/>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
File without changes
|