Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/astro virtual modules #183

Merged
merged 4 commits into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
Loading