Skip to content
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
15 changes: 2 additions & 13 deletions packages/create-jaad/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ import {
// Bumped together with the package they install.
const JAAD = "^0.9.3";
const ASTRO = "^7.3.5";
// Only the site template styles its own pages, so only it needs Tailwind.
const TEMPLATE_DEPENDENCIES = {
site: { "@tailwindcss/vite": "^4.3.3", tailwindcss: "^4.3.3" },
};
const ASTRO_CONFIG_NAMES = [
"astro.config.ts",
"astro.config.mjs",
Expand Down Expand Up @@ -211,7 +207,7 @@ function inspectExistingProject(target) {
* we create: adding it to someone's library would change how Node reads every
* file in it, and `astro.config.mjs` is ESM either way.
*/
function writeManifest(target, existing, name, template) {
function writeManifest(target, existing, name) {
const manifest = existing ?? { name, private: true, type: "module" };

manifest.scripts = {
Expand All @@ -233,13 +229,6 @@ function writeManifest(target, existing, name, template) {
) {
dependencies["@lancher-dev/jaad"] = JAAD;
}
for (const [pkg, range] of Object.entries(
TEMPLATE_DEPENDENCIES[template] ?? {},
)) {
if (!(pkg in dependencies) && !(pkg in (manifest.devDependencies ?? {}))) {
dependencies[pkg] = range;
}
}

if (Object.keys(dependencies).length > 0)
manifest.dependencies = dependencies;
Expand Down Expand Up @@ -418,7 +407,7 @@ function scaffold(request) {
write(target, destination, withTitle(source, title), written, skipped);
}

writeManifest(target, manifest, basename(target), template);
writeManifest(target, manifest, basename(target));
return { written, skipped, found };
}

Expand Down
24 changes: 12 additions & 12 deletions packages/create-jaad/templates/site/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@ Build the site with `astro build`.

## Layout

| Path | What it is |
| ---------------- | ----------------------------------------------- |
| `docs/` | The markdown that becomes the site |
| `jaad.config.ts` | Every JAAD option |
| `src/jaad.css` | Colours, fonts and widths (create it if absent) |

JAAD's own stylesheet is precompiled, so it brings no Tailwind with it. The
landing page's Tailwind is this project's own, wired up in `jaad.config.ts`.
| `astro.config.mjs` | Re-exports JAAD's Astro config; leave it alone |
| `src/content.config.ts` | Re-exports JAAD's collection; leave it alone |
| `src/pages/` | Pages of your own, starting with the landing page |
| `src/layouts/` | The layout those pages use |
| Path | What it is |
| ----------------------- | ------------------------------------------------- |
| `docs/` | The markdown that becomes the site |
| `jaad.config.ts` | Every JAAD option |
| `src/jaad.css` | Colours, fonts and widths (create it if absent) |
| `astro.config.mjs` | Re-exports JAAD's Astro config; leave it alone |
| `src/content.config.ts` | Re-exports JAAD's collection; leave it alone |
| `src/pages/` | Pages of your own, starting with the landing page |
| `src/layouts/` | The layout those pages use |

JAAD's stylesheet is precompiled; the landing page uses plain CSS
(`src/styles/site.css`). For Tailwind, see `tailwind` in the JAAD docs.

Pages in `src/pages/` keep their own layout, styles and metadata: JAAD's
options apply to documentation pages only. `routeBase: "/docs"` in
Expand Down
3 changes: 0 additions & 3 deletions packages/create-jaad/templates/site/jaad.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import tailwindcss from "@tailwindcss/vite";
import { defineJaadConfig } from "@lancher-dev/jaad";

export default defineJaadConfig({
title: "__JAAD_TITLE__",
routeBase: "/docs",
nav: [{ label: "Docs", href: "/docs" }],
// The landing page is styled with Tailwind; the documentation is not.
astro: { vite: { plugins: [tailwindcss()] } },
});
14 changes: 12 additions & 2 deletions packages/jaad/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,25 @@
"node": ">=22.12.0"
},
"peerDependencies": {
"astro": ">=7.0.0 <8.0.0"
"astro": ">=7.0.0 <8.0.0",
"tailwindcss": "^4.3.0",
"@tailwindcss/vite": "^4.3.0"
},
"peerDependenciesMeta": {
"tailwindcss": {
"optional": true
},
"@tailwindcss/vite": {
"optional": true
}
},
"dependencies": {
"@astrojs/sitemap": "^3.7.3",
"@lancher-dev/jaamd": "^0.9.2",
"github-slugger": "^2.0.0"
},
"scripts": {
"build": "tailwindcss -i ./src/styles/global.css -o ./dist/jaad.css",
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tailwindcss -i ./src/styles/global.css -o ./dist/jaad.css",
"prepare": "pnpm build",
"prepack": "pnpm build",
"check": "astro check"
Expand Down
11 changes: 10 additions & 1 deletion packages/jaad/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ export const jaadConfigSchema = z.object({
})
.default("auto"),

/** Registers Tailwind (the site's own install) and exposes JAAD's tokens to it. */
tailwind: z.boolean().default(false),

/** Per-locale overrides for JAAD's bundled interface strings. */
ui: z.record(z.string(), z.record(z.string(), z.string())).default({}),

Expand Down Expand Up @@ -247,7 +250,12 @@ const styleInPackage = (name: string) =>
export function resolveStylesheets(
config: JaadConfig,
cwd: string = process.cwd(),
): { user: string | null; theme: string | null; bridge: string } {
): {
user: string | null;
theme: string | null;
bridge: string;
tailwind: string | null;
} {
const userCss = join(cwd, "src", "jaad.css");
const slug =
typeof config.theme === "string" ? PRESETS[config.theme].theme : null;
Expand All @@ -260,6 +268,7 @@ export function resolveStylesheets(
: null,
// A preset supplies the palette, so the colours travel the other way.
bridge: styleInPackage(slug ? "jaamd-reverse.css" : "jaamd-forward.css"),
tailwind: config.tailwind ? styleInPackage("tailwind.css") : null,
};
}

Expand Down
53 changes: 47 additions & 6 deletions packages/jaad/src/integration.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { AstroIntegration } from "astro";
import type { AstroIntegration, AstroUserConfig } from "astro";
import { existsSync, readFileSync, readdirSync } from "node:fs";
import { createRequire } from "node:module";
import { join, relative, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";

import type { JaadResolvedConfig } from "./config.ts";
import type { JaadResolvedConfig, resolveStylesheets } from "./config.ts";
import { fonts } from "./fonts.ts";
import { jaadVirtualPlugin } from "./virtual.ts";
import {
Expand All @@ -19,10 +20,41 @@ import {
} from "./urls.ts";
import { prefixedLocales } from "./locales.ts";

interface Stylesheets {
user: string | null;
theme: string | null;
bridge: string;
type Stylesheets = ReturnType<typeof resolveStylesheets>;
type VitePlugins = NonNullable<NonNullable<AstroUserConfig["vite"]>["plugins"]>;

const NOT_INSTALLED = new Set([
"MODULE_NOT_FOUND",
"ERR_PACKAGE_PATH_NOT_EXPORTED",
]);

/** The site's own `@tailwindcss/vite`, unless it already registered one. */
export function loadTailwindPlugin(
root: string,
registered: VitePlugins | undefined,
): VitePlugins {
const plugins = ([registered] as unknown[]).flat(Infinity) as ({
name?: string;
} | null)[];
if (plugins.some((p) => p?.name?.startsWith("@tailwindcss/vite"))) return [];

const load = createRequire(join(root, "package.json"));
let tailwindcss: () => VitePlugins;
try {
load.resolve("tailwindcss/package.json");
({ default: tailwindcss } = load("@tailwindcss/vite") as {
default: () => VitePlugins;
});
} catch (error) {
if (!NOT_INSTALLED.has((error as { code?: string }).code ?? ""))
throw error;
throw new Error(
"jaad: `tailwind: true` needs Tailwind in your project:\n" +
" npm i -D tailwindcss @tailwindcss/vite",
{ cause: error },
);
}
return tailwindcss();
}

/** One per locale: each has its own first page, and its own redirect. */
Expand Down Expand Up @@ -193,8 +225,17 @@ export function createCoreIntegration(
stylesheets.user,
stylesheets.theme,
stylesheets.bridge,
stylesheets.tailwind,
() => state.docsFrame,
),
...(config.tailwind
? [
loadTailwindPlugin(
fileURLToPath(astroConfig.root),
astroConfig.vite.plugins,
),
]
: []),
],
// Package .astro sources must reach the Astro compiler.
ssr: { noExternal: ["@lancher-dev/jaad"] },
Expand Down
1 change: 1 addition & 0 deletions packages/jaad/src/layouts/Docs.astro
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import "@lancher-dev/jaamd/styles.css";
import "virtual:jaad/theme.css";
import "virtual:jaad/bridge.css";
import "virtual:jaad/user.css";
import "virtual:jaad/tailwind.css";

interface Props {
title?: string;
Expand Down
1 change: 1 addition & 0 deletions packages/jaad/src/layouts/Page.astro
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import "../../dist/jaad.css";
import "virtual:jaad/theme.css";
import "virtual:jaad/bridge.css";
import "virtual:jaad/user.css";
import "virtual:jaad/tailwind.css";

interface Props {
title?: string;
Expand Down
30 changes: 30 additions & 0 deletions packages/jaad/src/styles/tailwind.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* Opt-in (`tailwind: true`): site utilities read JAAD's tokens. No values, no preflight. */
@import "tailwindcss/theme.css" layer(theme) theme(reference);
@import "tailwindcss/utilities.css" layer(utilities);

@custom-variant dark (&:where(.dark, .dark *));

@theme reference {
--color-background: transparent;
--color-surface: transparent;
--color-foreground: transparent;
--color-foreground-bright: transparent;
--color-primary: transparent;
--color-error: transparent;
--color-warning: transparent;
--color-info: transparent;
--color-success: transparent;
--color-background-secondary: transparent;
--color-background-tertiary: transparent;
--color-surface-hover: transparent;
--color-border-light: transparent;
--color-border: transparent;
--color-border-dark: transparent;
--color-foreground-muted: transparent;
--color-foreground-secondary: transparent;
--color-primary-dark: transparent;
--color-primary-light: transparent;
--font-sans: transparent;
--font-serif: transparent;
--font-mono: transparent;
}
13 changes: 12 additions & 1 deletion packages/jaad/src/virtual.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ const RESOLVED_THEME_CSS = "\0" + THEME_CSS_ID;
// Which way the palette travels between JAAD and JAAMD; always one of the two.
const BRIDGE_CSS_ID = "virtual:jaad/bridge.css";

const TAILWIND_CSS_ID = "virtual:jaad/tailwind.css";
const RESOLVED_TAILWIND_CSS = "\0" + TAILWIND_CSS_ID;

const DOCS_FRAME_ID = "virtual:jaad/docs-frame";
const RESOLVED_DOCS_FRAME = "\0" + DOCS_FRAME_ID;

Expand All @@ -20,6 +23,7 @@ export function jaadVirtualPlugin(
userCss: string | null,
themeCss: string | null,
bridgeCss: string,
tailwindCss: string | null,
docsFrame: () => string,
) {
return {
Expand All @@ -30,13 +34,20 @@ export function jaadVirtualPlugin(
if (source === USER_CSS_ID) return userCss ?? RESOLVED_USER_CSS;
if (source === THEME_CSS_ID) return themeCss ?? RESOLVED_THEME_CSS;
if (source === BRIDGE_CSS_ID) return bridgeCss;
if (source === TAILWIND_CSS_ID)
return tailwindCss ?? RESOLVED_TAILWIND_CSS;
if (source === DOCS_FRAME_ID) return RESOLVED_DOCS_FRAME;
return null;
},
load(id: string) {
if (id === RESOLVED_ID)
return `export default ${JSON.stringify(config)};`;
if (id === RESOLVED_USER_CSS || id === RESOLVED_THEME_CSS) return "";
if (
id === RESOLVED_USER_CSS ||
id === RESOLVED_THEME_CSS ||
id === RESOLVED_TAILWIND_CSS
)
return "";
if (id === RESOLVED_DOCS_FRAME)
return `export { default } from ${JSON.stringify(docsFrame())};`;
return null;
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 12 additions & 1 deletion tests/build/css-coverage.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { allCss, packageClasses, missingFrom } from "../dist.mjs";
import { allCss, packageClasses, missingFrom, themeTokens } from "../dist.mjs";

const SRC = join(
dirname(fileURLToPath(import.meta.url)),
Expand All @@ -21,6 +21,17 @@ test("every utility used by the package ships in the built CSS", () => {
);
});

// `tailwind: true` exposes exactly JAAD's colour and font tokens.
test("tailwind.css names every colour and font token", () => {
const source = (name) => readFileSync(join(SRC, name), "utf8");
const pick = (tokens) =>
tokens.filter((t) => /^--(color|font)-/.test(t)).sort();
assert.deepEqual(
pick(themeTokens(source("styles/tailwind.css"))),
pick(themeTokens(source("styles/global.css"))),
);
});

// An unlayered rule beats every Tailwind utility, including the consumer's own.
test("the package styles no bare element outside a cascade layer", () => {
const css = allCss();
Expand Down
24 changes: 8 additions & 16 deletions tests/consumer/create.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -368,29 +368,21 @@ test("--here refuses the site template over a configuration it cannot read", ()
// forgets them, and everything fails once published.
// The scaffolder pins the version it installs by hand, and a release that
// bumps the package without it ships a project on the previous version.
// JAAD no longer puts Tailwind in anyone's Vite config, so only the template
// that styles its own pages may depend on it.
test("only the site template depends on tailwind", () => {
// Tailwind is opt-in (`tailwind: true`): no template installs it.
test("no template depends on tailwind", () => {
const parent = temporaryDirectory("jaad-template-deps-");
const deps = (template) => {
for (const template of ["docs", "site"]) {
const dir = join(parent, template);
run(
"node",
[CLI, dir, "--template", template, "--title", "T", "--no-install"],
parent,
);
return JSON.parse(readFileSync(join(dir, "package.json"), "utf8"))
.dependencies;
};

assert.ok(
!("tailwindcss" in deps("docs")),
"the docs template pulls tailwind",
);
assert.ok(
"tailwindcss" in deps("site"),
"the site template cannot style itself",
);
const { dependencies } = JSON.parse(
readFileSync(join(dir, "package.json"), "utf8"),
);
assert.ok(!("tailwindcss" in dependencies), `${template} pulls tailwind`);
}
});

test("the version the scaffolder installs is the one being published", () => {
Expand Down
Loading
Loading