Skip to content

Commit

Permalink
Support remote and local assets in custom CSS
Browse files Browse the repository at this point in the history
(This includes images and fonts using url().)

Using onResolve (as suggested in #786 (comment))

closes #786
supersedes #788
  • Loading branch information
Fil committed May 20, 2024
1 parent 3135033 commit b03a1e0
Show file tree
Hide file tree
Showing 11 changed files with 107 additions and 10 deletions.
28 changes: 20 additions & 8 deletions src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export async function build(

// For cache-breaking we rename most assets to include content hashes.
const aliases = new Map<string, string>();
const plainaliases = new Map<string, string>();

// Add the search bundle and data, if needed.
if (config.search) {
Expand All @@ -106,6 +107,7 @@ export async function build(

// Generate the client bundles (JavaScript and styles). TODO Use a content
// hash, or perhaps the Framework version number for built-in modules.
const delayedStylesheets = new Set<string>();
if (addPublic) {
for (const path of globalImports) {
if (path.startsWith("/_observablehq/") && path.endsWith(".js")) {
Expand Down Expand Up @@ -136,14 +138,9 @@ export async function build(
const sourcePath = await populateNpmCache(root, path); // TODO effects
await effects.copyFile(sourcePath, path);
} else if (!/^\w+:/.test(specifier)) {
const sourcePath = join(root, specifier);
effects.output.write(`${faint("build")} ${sourcePath} ${faint("→")} `);
const contents = await bundleStyles({path: sourcePath, minify: true});
const hash = createHash("sha256").update(contents).digest("hex").slice(0, 8);
const ext = extname(specifier);
const alias = `/${join("_import", dirname(specifier), `${basename(specifier, ext)}.${hash}${ext}`)}`;
aliases.set(resolveStylesheetPath(root, specifier), alias);
await effects.writeFile(alias, contents);
// Uses a side effect to register file assets on custom stylesheets
delayedStylesheets.add(specifier);
await bundleStyles({path: join(root, specifier), files});
}
}
}
Expand All @@ -170,9 +167,24 @@ export async function build(
const ext = extname(file);
const alias = `/${join("_file", dirname(file), `${basename(file, ext)}.${hash}${ext}`)}`;
aliases.set(loaders.resolveFilePath(file), alias);
plainaliases.set(file, alias);
await effects.writeFile(alias, contents);
}

// Write delayed stylesheets
if (addPublic) {
for (const specifier of delayedStylesheets) {
const sourcePath = join(root, specifier);
effects.output.write(`${faint("build")} ${sourcePath} ${faint("→")} `);
const contents = await bundleStyles({path: sourcePath, minify: true, aliases: plainaliases});
const hash = createHash("sha256").update(contents).digest("hex").slice(0, 8);
const ext = extname(specifier);
const alias = `/${join("_import", dirname(specifier), `${basename(specifier, ext)}.${hash}${ext}`)}`;
aliases.set(resolveStylesheetPath(root, specifier), alias);
await effects.writeFile(alias, contents);
}
}

// Download npm imports. TODO It might be nice to use content hashes for
// these, too, but it would involve rewriting the files since populateNpmCache
// doesn’t let you pass in a resolver.
Expand Down
22 changes: 20 additions & 2 deletions src/rollup.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {extname} from "node:path/posix";
import {extname, join} from "node:path/posix";
import {nodeResolve} from "@rollup/plugin-node-resolve";
import type {CallExpression} from "acorn";
import {simple} from "acorn-walk";
import type {PluginBuild} from "esbuild";
import {build} from "esbuild";
import type {AstNode, OutputChunk, Plugin, ResolveIdResult} from "rollup";
import {rollup} from "rollup";
Expand Down Expand Up @@ -36,16 +37,33 @@ function rewriteInputsNamespace(code: string) {
export async function bundleStyles({
minify = false,
path,
theme
theme,
files,
aliases
}: {
minify?: boolean;
path?: string;
theme?: string[];
files?: Set<string>;
aliases?: Map<string, string>;
}): Promise<string> {
const assets = {
name: "resolve CSS assets",
setup(build: PluginBuild) {
build.onResolve({filter: /^\w+:\/\//}, (args) => ({path: args.path, external: true}));
build.onResolve({filter: /./}, (args) => {
if (args.path.endsWith(".css") || args.path.match(/^[#.]/)) return;
if (files) files.add(args.path); // /!\ modifies files as a side effect
const path = join("..", aliases?.get(args.path) ?? join("_file", args.path));
return {path, external: true};
});
}
};
const result = await build({
bundle: true,
...(path ? {entryPoints: [path]} : {stdin: {contents: renderTheme(theme!), loader: "css"}}),
write: false,
plugins: [assets],
minify,
alias: STYLE_MODULES
});
Expand Down
Binary file added test/input/build/css-public/horse.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions test/input/build/css-public/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
style: style.css
---

# CSS assets

Atkinson Hyperlegible font is named after Braille Institute founder, J. Robert Atkinson. What makes it different from traditional typography design is that it focuses on letterform distinction to increase character recognition, ultimately improving readability. [We are making it free for anyone to use!](https://brailleinstitute.org/freefont)

<figure>
<div class="bg" style="height: 518px;"></div>
<figcaption>This image is set with CSS.</figcaption>
</figure>
20 changes: 20 additions & 0 deletions test/input/build/css-public/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
@import url("observablehq:default.css");
@import url("observablehq:theme-air.css");

:root {
--serif: "Atkinson Hyperlegible";
}

div.bg {
background-image: url("horse.jpg");
}

div.dont-break-hashes {
offset-path: url(#path);
}

@font-face {
font-family: "Atkinson Hyperlegible";
src: url(https://fonts.gstatic.com/s/atkinsonhyperlegible/v11/9Bt23C1KxNDXMspQ1lPyU89-1h6ONRlW45G04pIoWQeCbA.woff2)
format("woff2");
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions test/output/build/css-public/_import/style.a31bcaf4.css

Large diffs are not rendered by default.

Empty file.
Empty file.
Empty file.
34 changes: 34 additions & 0 deletions test/output/build/css-public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!DOCTYPE html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>CSS assets</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="./_import/style.a31bcaf4.css">
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="./_import/style.a31bcaf4.css">
<link rel="modulepreload" href="./_observablehq/client.js">
<link rel="modulepreload" href="./_observablehq/runtime.js">
<link rel="modulepreload" href="./_observablehq/stdlib.js">
<script type="module">

import "./_observablehq/client.js";

</script>
<aside id="observablehq-toc" data-selector="h1:not(:first-of-type)[id], h2:first-child[id], :not(h1) + h2[id]">
<nav>
</nav>
</aside>
<div id="observablehq-center">
<main id="observablehq-main" class="observablehq">
<h1 id="css-assets" tabindex="-1"><a class="observablehq-header-anchor" href="#css-assets">CSS assets</a></h1>
<p>Atkinson Hyperlegible font is named after Braille Institute founder, J. Robert Atkinson. What makes it different from traditional typography design is that it focuses on letterform distinction to increase character recognition, ultimately improving readability. <a href="https://brailleinstitute.org/freefont" target="_blank" rel="noopener noreferrer">We are making it free for anyone to use!</a></p>
<figure>
<div class="bg" style="height: 518px;"></div>
<figcaption>This image is set with CSS.</figcaption>
</figure>
</main>
<footer id="observablehq-footer">
<div>Built with <a href="https://observablehq.com/" target="_blank" rel="noopener noreferrer">Observable</a> on <a title="2024-01-10T16:00:00">Jan 10, 2024</a>.</div>
</footer>
</div>

0 comments on commit b03a1e0

Please sign in to comment.