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

Support remote and local assets in custom CSS #1372

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
40 changes: 32 additions & 8 deletions src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,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 localStylesheets = new Set<string>();
if (addPublic) {
for (const path of globalImports) {
if (path.startsWith("/_observablehq/") && path.endsWith(".js")) {
Expand Down Expand Up @@ -138,14 +139,16 @@ 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);
localStylesheets.add(specifier);
await bundleStyles({
path: join(root, specifier),
resolve(args) {
if (args.path.endsWith(".css") || args.path.match(/^[#?]/) || args.path.match(/^\w+:/)) return;
files.add(args.path);
loaders.resolveFilePath(args.path);
return {path: "(pending)", external: true};
}
});
}
}
}
Expand Down Expand Up @@ -176,6 +179,27 @@ export async function build(
await effects.writeFile(alias, contents);
}

// Write local stylesheets.
if (addPublic) {
for (const specifier of localStylesheets) {
const sourcePath = join(root, specifier);
effects.output.write(`${faint("build")} ${sourcePath} ${faint("→")} `);
const contents = await bundleStyles({
path: sourcePath,
minify: true,
resolve(args) {
if (args.path.endsWith(".css") || args.path.match(/^[#?]/) || args.path.match(/^\w+:/)) return;
return {path: join("..", aliases.get(loaders.resolveFilePath(args.path))!), external: 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);
}
}

// 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
14 changes: 13 additions & 1 deletion src/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,19 @@ export class PreviewServer {
try {
if (pathname.endsWith(".css")) {
await access(filepath, constants.R_OK);
end(req, res, await bundleStyles({path: filepath}), "text/css");
end(
req,
res,
await bundleStyles({
path: filepath,
resolve(args) {
if (args.path.endsWith(".css") || args.path.match(/^[#?]/) || args.path.match(/^\w+:/)) return;
const path = loaders.resolveFilePath(args.path);
return {path, external: true};
}
}),
"text/css"
);
return;
} else if (pathname.endsWith(".js")) {
const input = await readJavaScript(join(root, path));
Expand Down
13 changes: 12 additions & 1 deletion src/rollup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,27 @@ function rewriteInputsNamespace(code: string) {
export async function bundleStyles({
minify = false,
path,
theme
theme,
resolve
}: {
minify?: boolean;
path?: string;
theme?: string[];
resolve?: ({path}: {path: string}) => {path: string; external: true} | undefined;
}): Promise<string> {
const result = await build({
bundle: true,
...(path ? {entryPoints: [path]} : {stdin: {contents: renderTheme(theme!), loader: "css"}}),
write: false,
plugins: [
{
name: "resolve CSS assets",
setup(build) {
build.onResolve({filter: /^\w+:\/\//}, ({path}) => ({path, external: true}));
if (resolve) build.onResolve({filter: /./}, resolve);
}
}
],
minify,
alias: STYLE_MODULES
});
Expand Down
9 changes: 9 additions & 0 deletions test/input/build/css-public/atkinson.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@font-face {
font-family: "Atkinson Hyperlegible";
src: url(https://fonts.gstatic.com/s/atkinsonhyperlegible/v11/9Bt23C1KxNDXMspQ1lPyU89-1h6ONRlW45G04pIoWQeCbA.woff2)
format("woff2");
}

:root {
--serif: "Atkinson Hyperlegible";
}
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>
11 changes: 11 additions & 0 deletions test/input/build/css-public/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@import url("observablehq:default.css");
@import url("observablehq:theme-air.css");
@import url("atkinson.css");

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

div.dont-break-hashes {
offset-path: url(#path);
}
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.538adc61.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.538adc61.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.538adc61.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>
Loading