Skip to content

Commit

Permalink
Merge pull request #183 from CloudCannon/feat/astro-virtual-modules
Browse files Browse the repository at this point in the history
Feat/astro virtual modules
  • Loading branch information
bglw committed Dec 22, 2023
2 parents ba73f47 + 51e4aec commit 80d146d
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 45 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@

## Unreleased

* Adds support for the `astro:content` and `astro:assets` modules inside Bookshop components.
* Adds support for the `<slot/>` 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.
Expand Down
96 changes: 79 additions & 17 deletions javascript-modules/engines/astro-engine/lib/builder.js
Original file line number Diff line number Diff line change
@@ -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"];

Expand All @@ -13,32 +13,59 @@ 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",
async setup(build) {
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],
Expand All @@ -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",
Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
}

Expand All @@ -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 {
Expand All @@ -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
}
});
},
},
];
Expand Down
91 changes: 63 additions & 28 deletions javascript-modules/engines/astro-engine/lib/engine.js
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
Expand All @@ -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\/(?<collection>[^/]*)/)?.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:')]",
Expand Down
5 changes: 5 additions & 0 deletions javascript-modules/engines/astro-engine/lib/modules/assets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import ImageInternal from './image.astro';
import PictureInternal from './picture.astro';

export const Image = ImageInternal;
export const Picture = PictureInternal;
45 changes: 45 additions & 0 deletions javascript-modules/engines/astro-engine/lib/modules/content.js
Original file line number Diff line number Diff line change
@@ -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 });
};
23 changes: 23 additions & 0 deletions javascript-modules/engines/astro-engine/lib/modules/image.astro
Original file line number Diff line number Diff line change
@@ -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);
}
---

<img
src={props.src}
alt={props.alt}
width={props.width}
height={props.height}
/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
import Image from "./image.astro";
---

<picture>
<Image {...Astro.props} />
</picture>

0 comments on commit 80d146d

Please sign in to comment.