Skip to content

Commit 55fabb4

Browse files
committed
tailwind
1 parent 21ac188 commit 55fabb4

File tree

7 files changed

+430
-21
lines changed

7 files changed

+430
-21
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
"cross-env": "^7.0.3",
120120
"d3-dsv": "^3.0.1",
121121
"d3-format": "^3.1.0",
122+
"esbuild-plugin-tailwindcss": "^1.2.1",
122123
"eslint": "^8.50.0",
123124
"eslint-config-prettier": "^9.1.0",
124125
"eslint-import-resolver-typescript": "^3.6.1",

src/build.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,10 @@ export async function build(
164164
effects.output.write(`${faint("build")} ${path} ${faint("→")} `);
165165
if (specifier.startsWith("observablehq:theme-")) {
166166
const match = /^observablehq:theme-(?<theme>[\w-]+(,[\w-]+)*)?\.css$/.exec(specifier);
167-
contents = await bundleStyles({theme: match!.groups!.theme?.split(",") ?? [], minify: true});
167+
contents = await bundleStyles({theme: match!.groups!.theme?.split(",") ?? [], minify: true, root});
168168
} else {
169169
const clientPath = getClientPath(path.slice("/_observablehq/".length));
170-
contents = await bundleStyles({path: clientPath, minify: true});
170+
contents = await bundleStyles({path: clientPath, minify: true, root});
171171
}
172172
const hash = createHash("sha256").update(contents).digest("hex").slice(0, 8);
173173
const alias = applyHash(path, hash);
@@ -181,7 +181,7 @@ export async function build(
181181
} else if (!/^\w+:/.test(specifier)) {
182182
const sourcePath = join(root, specifier);
183183
effects.output.write(`${faint("build")} ${sourcePath} ${faint("→")} `);
184-
const contents = await bundleStyles({path: sourcePath, minify: true});
184+
const contents = await bundleStyles({path: sourcePath, minify: true, root});
185185
const hash = createHash("sha256").update(contents).digest("hex").slice(0, 8);
186186
const alias = applyHash(join("/_import", specifier), hash);
187187
aliases.set(resolveStylesheetPath(root, specifier), alias);

src/preview.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,13 @@ export class PreviewServer {
132132
} else if (pathname === "/_observablehq/minisearch.json") {
133133
end(req, res, await searchIndex(config), "application/json");
134134
} else if ((match = /^\/_observablehq\/theme-(?<theme>[\w-]+(,[\w-]+)*)?\.css$/.exec(pathname))) {
135-
end(req, res, await bundleStyles({theme: match.groups!.theme?.split(",") ?? []}), "text/css");
135+
end(req, res, await bundleStyles({theme: match.groups!.theme?.split(",") ?? [], root}), "text/css");
136136
} else if (pathname.startsWith("/_observablehq/") && pathname.endsWith(".js")) {
137137
const path = getClientPath(pathname.slice("/_observablehq/".length));
138138
end(req, res, await rollupClient(path, root, pathname), "text/javascript");
139139
} else if (pathname.startsWith("/_observablehq/") && pathname.endsWith(".css")) {
140140
const path = getClientPath(pathname.slice("/_observablehq/".length));
141-
end(req, res, await bundleStyles({path}), "text/css");
141+
end(req, res, await bundleStyles({path, root}), "text/css");
142142
} else if (pathname.startsWith("/_node/") || pathname.startsWith("/_jsr/")) {
143143
send(req, pathname, {root: join(root, ".observablehq", "cache")}).pipe(res);
144144
} else if (pathname.startsWith("/_npm/")) {
@@ -151,7 +151,7 @@ export class PreviewServer {
151151
if (module) {
152152
const sourcePath = join(root, path);
153153
await access(sourcePath, constants.R_OK);
154-
end(req, res, await bundleStyles({path: sourcePath}), "text/css");
154+
end(req, res, await bundleStyles({path: sourcePath, root}), "text/css");
155155
return;
156156
}
157157
} else if (pathname.endsWith(".js")) {

src/rollup.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
import {writeFileSync} from "fs";
12
import {extname, resolve} from "node:path/posix";
23
import {nodeResolve} from "@rollup/plugin-node-resolve";
34
import {simple} from "acorn-walk";
45
import {build} from "esbuild";
6+
import type {Plugin as ESBuildPlugin} from "esbuild";
7+
import {tailwindPlugin} from "esbuild-plugin-tailwindcss";
58
import type {AstNode, OutputChunk, Plugin, ResolveIdResult} from "rollup";
69
import {rollup} from "rollup";
710
import esbuild from "rollup-plugin-esbuild";
8-
import {getClientPath, getStylePath} from "./files.js";
11+
import {getClientPath, getStylePath, maybeStat} from "./files.js";
912
import type {StringLiteral} from "./javascript/source.js";
1013
import {getStringLiteralValue, isStringLiteral} from "./javascript/source.js";
1114
import {resolveNpmImport} from "./npm.js";
@@ -36,17 +39,20 @@ function rewriteInputsNamespace(code: string) {
3639
export async function bundleStyles({
3740
minify = false,
3841
path,
39-
theme
42+
theme,
43+
root
4044
}: {
4145
minify?: boolean;
4246
path?: string;
4347
theme?: string[];
48+
root: string;
4449
}): Promise<string> {
4550
const result = await build({
4651
bundle: true,
4752
...(path ? {entryPoints: [path]} : {stdin: {contents: renderTheme(theme!), loader: "css"}}),
4853
write: false,
4954
minify,
55+
plugins: [await tailwindConfig(root)],
5056
alias: STYLE_MODULES
5157
});
5258
let text = result.outputFiles[0].text;
@@ -184,3 +190,34 @@ function importMetaResolve(path: string, resolveImport: ImportResolver): Plugin
184190
}
185191
};
186192
}
193+
194+
// Create a tailwind plugin, configured to reference as content the project
195+
// files that might contain tailwind class names, and the 'tw-' prefix. If a
196+
// tailwind.config.js is present in the project root, we import and merge it.
197+
async function tailwindConfig(root: string): Promise<ESBuildPlugin> {
198+
const twconfig = "tailwind.config.js";
199+
const configPath = `./${root}/.observablehq/cache/${twconfig}`;
200+
const s = await maybeStat(`./${root}/${twconfig}`);
201+
const m = await maybeStat(configPath);
202+
if (!m || !s || !(m.mtimeMs > s.mtimeMs)) {
203+
writeFileSync(
204+
configPath,
205+
`
206+
// File generated by rollup.ts; to configure tailwind, edit ${root}/tailwind.config.js
207+
${s ? `import config from "../../${twconfig}"` : "const config = {}"};
208+
export default {
209+
content: {
210+
files: [
211+
"${root}/**/*.{js,md}" /* pages and components */,
212+
"${root}/.observablehq/cache/**/*.md" /* page loaders */,
213+
"${root}/.observablehq/cache/_import/**/*.js" /* transpiled components */
214+
]
215+
},
216+
prefix: 'tw-',
217+
...config
218+
};
219+
`
220+
);
221+
}
222+
return tailwindPlugin({configPath});
223+
}

src/style/default.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
@import url("./tailwind.css");
12
@import url("./global.css");
23
@import url("./layout.css");
34
@import url("./grid.css");

src/style/tailwind.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/* @import "tailwindcss/base"; */
2+
@import "tailwindcss/components";
3+
@import "tailwindcss/utilities";

0 commit comments

Comments
 (0)