-
Notifications
You must be signed in to change notification settings - Fork 162
Support remote and local assets in custom CSS #1372
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
base: main
Are you sure you want to change the base?
Changes from 2 commits
b03a1e0
43ed43b
bc8f05b
b773833
3263185
f6a1924
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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>(); | ||
Fil marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// Add the search bundle and data, if needed. | ||
if (config.search) { | ||
|
@@ -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")) { | ||
|
@@ -123,11 +125,11 @@ export async function build( | |
effects.output.write(`${faint("build")} ${specifier} ${faint("→")} `); | ||
if (specifier.startsWith("observablehq:theme-")) { | ||
const match = /^observablehq:theme-(?<theme>[\w-]+(,[\w-]+)*)?\.css$/.exec(specifier); | ||
const contents = await bundleStyles({theme: match!.groups!.theme?.split(",") ?? [], minify: true}); | ||
const {contents} = await bundleStyles({theme: match!.groups!.theme?.split(",") ?? [], minify: true}); | ||
await effects.writeFile(path, contents); | ||
} else { | ||
const clientPath = getClientPath(path.slice("/_observablehq/".length)); | ||
const contents = await bundleStyles({path: clientPath, minify: true}); | ||
const {contents} = await bundleStyles({path: clientPath, minify: true}); | ||
await effects.writeFile(`/_observablehq/${specifier.slice("observablehq:".length)}`, contents); | ||
} | ||
} else if (specifier.startsWith("npm:")) { | ||
|
@@ -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); | ||
Fil marked this conversation as resolved.
Show resolved
Hide resolved
|
||
for (const file of (await bundleStyles({path: join(root, specifier)})).files) files.add(file); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be cleaner to have two separate methods (that use the same underlying implementation) since we always call |
||
} | ||
} | ||
} | ||
|
@@ -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 | ||
Fil marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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. | ||
|
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"; | ||
|
@@ -36,21 +37,38 @@ function rewriteInputsNamespace(code: string) { | |
export async function bundleStyles({ | ||
minify = false, | ||
path, | ||
theme | ||
theme, | ||
aliases | ||
}: { | ||
minify?: boolean; | ||
path?: string; | ||
theme?: string[]; | ||
}): Promise<string> { | ||
aliases?: Map<string, string>; | ||
}): Promise<{contents: string; files: Set<string>}> { | ||
const files = new Set<string>(); | ||
const assets = { | ||
Fil marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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; | ||
files.add(args.path); | ||
const path = join("..", aliases?.get(args.path) ?? join("_file", args.path)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another big advantage of the And for the preview command, ideally we’d supply a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now we pass the marked URL, but I wasn't able to positively test hot module replacement. |
||
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 | ||
}); | ||
const text = result.outputFiles[0].text; | ||
return rewriteInputsNamespace(text); // TODO only for inputs | ||
const contents = rewriteInputsNamespace(text); // TODO only for inputs | ||
return {contents, files}; | ||
} | ||
|
||
export async function rollupClient( | ||
|
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> |
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"); | ||
} |
Large diffs are not rendered by default.
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&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&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> |
Uh oh!
There was an error while loading. Please reload this page.