diff --git a/CHANGELOG.md b/CHANGELOG.md index 2aa2754d..7eca305c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ ## Unreleased +* Adds support for the `astro:content` and `astro:assets` modules inside Bookshop components. +* Adds support for the `` component and the `Astro.slots` global inside Bookshop components. +* Astro Bookshop will now use your configured Vite plugins when building components. + ## v3.8.2 (December 5, 2023) * Fixes an error in Astro Bookshop, when spreading a prop that is possibly undefined. diff --git a/javascript-modules/engines/astro-engine/lib/builder.js b/javascript-modules/engines/astro-engine/lib/builder.js index 3e4ef25a..0c04ec7b 100644 --- a/javascript-modules/engines/astro-engine/lib/builder.js +++ b/javascript-modules/engines/astro-engine/lib/builder.js @@ -1,10 +1,10 @@ import * as fs from "fs"; -import { join } from "path"; +import { join, dirname } from "path"; import { transform } from "@astrojs/compiler"; import AstroPluginVite from "@bookshop/vite-plugin-astro-bookshop"; import { resolveConfig } from "vite"; import * as esbuild from "esbuild"; -import { sassPlugin, postcssModules } from 'esbuild-sass-plugin' +import { sassPlugin, postcssModules } from "esbuild-sass-plugin"; export const extensions = [".astro", ".jsx", ".tsx"]; @@ -13,10 +13,10 @@ const { transform: bookshopTransform } = AstroPluginVite(); export const buildPlugins = [ sassPlugin({ filter: /\.module\.(s[ac]ss|css)$/, - transform: postcssModules({}) + transform: postcssModules({}), }), sassPlugin({ - filter: /\.(s[ac]ss|css)$/ + filter: /\.(s[ac]ss|css)$/, }), { name: "bookshop-astro", @@ -24,21 +24,48 @@ export const buildPlugins = [ let astroConfig; let defaultScopedStyleStrategy; try { - const astroPackageJSON = JSON.parse(await fs.promises.readFile(join(process.cwd(), 'node_modules', 'astro', 'package.json'), "utf8")) - defaultScopedStyleStrategy = astroPackageJSON.version.startsWith('2') - ? 'where' - : 'attribute'; - astroConfig = (await import(join(process.cwd(), 'astro.config.mjs'))).default; - }catch (err){ + const astroPackageJSON = JSON.parse( + await fs.promises.readFile( + join(process.cwd(), "node_modules", "astro", "package.json"), + "utf8" + ) + ); + defaultScopedStyleStrategy = astroPackageJSON.version.startsWith("2") + ? "where" + : "attribute"; + astroConfig = (await import(join(process.cwd(), "astro.config.mjs"))) + .default; + } catch (err) { astroConfig = {}; } + build.onResolve({ filter: /^astro:.*$/ }, async (args) => { + const type = args.path.replace("astro:", ""); + if (type !== "content" && type !== "assets") { + console.error( + `Error: The 'astro:${type}' module is not supported inside Bookshop components.` + ); + throw new Error("Unsupported virtual module"); + } + let dir = ""; + if (typeof __dirname !== "undefined") { + dir = __dirname; + } else { + dir = dirname(import.meta.url); + } + const path = join(dir, "modules", `${type}.js`).replace("file:", ""); + return { + path, + }; + }); + build.onLoad({ filter: /\.astro$/, namespace: "style" }, async (args) => { let text = await fs.promises.readFile(args.path, "utf8"); let transformed = await transform(text, { internalURL: "astro/runtime/server/index.js", filename: args.path.replace(process.cwd(), ""), - scopedStyleStrategy: astroConfig.scopedStyleStrategy ?? defaultScopedStyleStrategy + scopedStyleStrategy: + astroConfig.scopedStyleStrategy ?? defaultScopedStyleStrategy, }); return { contents: transformed.css[0], @@ -50,7 +77,8 @@ export const buildPlugins = [ let tsResult = await transform(text, { internalURL: "astro/runtime/server/index.js", filename: args.path.replace(process.cwd(), ""), - scopedStyleStrategy: astroConfig.scopedStyleStrategy ?? defaultScopedStyleStrategy + scopedStyleStrategy: + astroConfig.scopedStyleStrategy ?? defaultScopedStyleStrategy, }); let jsResult = await esbuild.transform(tsResult.code, { loader: "ts", @@ -63,8 +91,8 @@ export const buildPlugins = [ args.path.replace(process.cwd(), "") ); - if(!result){ - console.warn('Bookshop transform failed:', args.path); + if (!result) { + console.warn("Bookshop transform failed:", args.path); result = jsResult; } @@ -98,8 +126,8 @@ export const buildPlugins = [ args.path.replace(process.cwd(), "") ); - if(!result){ - console.warn('Bookshop transform failed:', args.path); + if (!result) { + console.warn("Bookshop transform failed:", args.path); result = jsResult; } @@ -114,7 +142,9 @@ export const buildPlugins = [ }; }); build.onLoad( - { filter: /astro(\/|\\)dist(\/|\\)runtime(\/|\\)server(\/|\\)index.js$/ }, + { + filter: /astro(\/|\\)dist(\/|\\)runtime(\/|\\)server(\/|\\)index.js$/, + }, async (args) => { let text = await fs.promises.readFile(args.path, "utf8"); return { @@ -129,6 +159,38 @@ export const buildPlugins = [ return { path: args.importer, namespace: "style" }; } ); + build.onLoad({ filter: /.*/ }, async (args) => { + try{ + if (astroConfig.vite?.plugins) { + const text = await fs.promises.readFile(args.path, "utf8"); + for (const plugin of astroConfig.vite.plugins) { + if (!plugin.transform) { + continue; + } + + const result = await plugin.transform( + text, + args.path.replace(process.cwd(), "") + ); + + if (!result) { + continue; + } + + if (typeof result !== "string" && !result.code) { + return; + } + + return { + contents: typeof result === "string" ? result : result.code, + loader: "js", + }; + } + } + } catch(err){ + // Intentionally ignored + } + }); }, }, ]; diff --git a/javascript-modules/engines/astro-engine/lib/engine.js b/javascript-modules/engines/astro-engine/lib/engine.js index a0ec553f..4b3e7d6b 100644 --- a/javascript-modules/engines/astro-engine/lib/engine.js +++ b/javascript-modules/engines/astro-engine/lib/engine.js @@ -1,4 +1,7 @@ -import { renderToString } from "astro/runtime/server/index.js"; +import { + renderToString, + renderSlotToString, +} from "astro/runtime/server/index.js"; import { processFrontmatter } from "@bookshop/astro-bookshop/helpers/frontmatter-helper"; import { createRoot } from "react-dom/client"; import { createElement } from "react"; @@ -96,36 +99,41 @@ export class Engine { async renderAstroComponent(target, key, props, globals) { const component = this.files?.[key]; - const result = await renderToString( - { - styles: new Set(), - scripts: new Set(), - links: new Set(), - propagation: new Map(), - propagators: new Map(), - extraHead: [], - componentMetadata: new Map(), + const SSRResult = { + styles: new Set(), + scripts: new Set(), + links: new Set(), + propagation: new Map(), + propagators: new Map(), + extraHead: [], + componentMetadata: new Map(), + renderers, + _metadata: { renderers, - _metadata: { - renderers, - hasHydrationScript: false, - hasRenderedHead: true, - hasDirectives: new Set(), - }, - slots: null, - props, - createAstro(astroGlobal, props, slots) { - return { - __proto__: astroGlobal, - props, - slots, - }; - }, + hasHydrationScript: false, + hasRenderedHead: true, + hasDirectives: new Set(), }, - component, + slots: null, props, - null - ); + createAstro(astroGlobal, props, slots) { + const astroSlots = { + has: (name) => { + if (!slots) return false; + return Boolean(slots[name]); + }, + render: (name) => { + return renderSlotToString(SSRResult, slots[name]); + }, + }; + return { + __proto__: astroGlobal, + props, + slots: astroSlots, + }; + }, + }; + const result = await renderToString(SSRResult, component, props, null); const doc = document.implementation.createHTMLDocument(); doc.body.innerHTML = result; this.updateBindings(doc); @@ -137,6 +145,33 @@ export class Engine { return str.split(".").reduce((curr, key) => curr?.[key], props[0]); } + async storeInfo(info = {}) { + const collections = info.collections || {}; + for (const [key, val] of Object.entries(collections)) { + const collectionKey = + val[0]?.path.match(/^\/?src\/content\/(?[^/]*)/)?.groups + .collection ?? key; + const collection = val.map((item) => { + let id = item.path.replace(`src/content/${collectionKey}/`, ""); + if (!id.match(/\.md(x|oc)?$/)) { + id = id.replace(/\..*$/, ""); + } + return { + id, + collection: collectionKey, + slug: item.slug ?? id.replace(/\..*$/, ""), + render: () => () => "Content is not available when live editing", + body: "Content is not available when live editing", + data: item, + }; + }); + collections[key] = collection; + collections[collectionKey] = collection; + } + + window.__bookshop_collections = collections; + } + getBindingCommentIterator(documentNode) { return documentNode.evaluate( "//comment()[contains(.,'databinding:')]", diff --git a/javascript-modules/engines/astro-engine/lib/modules/assets.js b/javascript-modules/engines/astro-engine/lib/modules/assets.js new file mode 100644 index 00000000..12fce2e0 --- /dev/null +++ b/javascript-modules/engines/astro-engine/lib/modules/assets.js @@ -0,0 +1,5 @@ +import ImageInternal from './image.astro'; +import PictureInternal from './picture.astro'; + +export const Image = ImageInternal; +export const Picture = PictureInternal; diff --git a/javascript-modules/engines/astro-engine/lib/modules/content.js b/javascript-modules/engines/astro-engine/lib/modules/content.js new file mode 100644 index 00000000..3c4e7a1e --- /dev/null +++ b/javascript-modules/engines/astro-engine/lib/modules/content.js @@ -0,0 +1,45 @@ +export const getCollection = (collectionKey, filter) => { + if (!window.__bookshop_collections) { + console.warn("[Bookshop] Failed to load site collections for live editing"); + return []; + } + + if (!window.__bookshop_collections[collectionKey]) { + console.warn("[Bookshop] Failed to load collection: ", collectionKey); + return []; + } + + if (filter) { + return window.__bookshop_collections[collectionKey].filter(filter); + } + return window.__bookshop_collections[collectionKey]; +}; + +export const getEntry = (...args) => { + if (args.length === 1) { + const { collection: collectionKey, slug: entrySlug, id: entryId } = args[0]; + const collection = getCollection(collectionKey); + if (entryId) { + return collection.find(({ id }) => id === entryId); + } else if (entrySlug) { + return collection.find(({ slug }) => slug === entrySlug); + } + return console.warn( + "[Bookshop] Failed to load entries, invalid arguments: ", + args + ); + } + + const [collectionKey, entryKey] = args; + const collection = getCollection(collectionKey); + + return collection.find(({ id, slug }) => entryKey === (slug ?? id)); +}; + +export const getEntries = (entries) => { + return entries.map(getEntry); +}; + +export const getEntryBySlug = (collection, slug) => { + return getEntry({ collection, slug }); +}; diff --git a/javascript-modules/engines/astro-engine/lib/modules/image.astro b/javascript-modules/engines/astro-engine/lib/modules/image.astro new file mode 100644 index 00000000..9f3b7214 --- /dev/null +++ b/javascript-modules/engines/astro-engine/lib/modules/image.astro @@ -0,0 +1,23 @@ +--- +const props = Astro.props; + +if (props.alt === undefined || props.alt === null) { + throw new Error("Image missing alt"); +} + +// As a convenience, allow width and height to be string with a number in them, to match HTML's native `img`. +if (typeof props.width === "string") { + props.width = parseInt(props.width); +} + +if (typeof props.height === "string") { + props.height = parseInt(props.height); +} +--- + +{props.alt} diff --git a/javascript-modules/engines/astro-engine/lib/modules/picture.astro b/javascript-modules/engines/astro-engine/lib/modules/picture.astro new file mode 100644 index 00000000..20d15198 --- /dev/null +++ b/javascript-modules/engines/astro-engine/lib/modules/picture.astro @@ -0,0 +1,7 @@ +--- +import Image from "./image.astro"; +--- + + + +