Files
WRNexusJS/packages/styles/src/index.ts
T
Clintchiz 586a6db8ff
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
release: WRNexusJS 0.8.0
2026-08-02 23:18:51 +05:30

177 lines
5.5 KiB
TypeScript

/**
* @wrnexus/styles — global stylesheet pipeline + app config.
*
* Works for SSR and CSR: the bundled stylesheet is `<link>`ed into every page's
* `<head>`, so it styles server-rendered markup and hydrated client islands
* alike. Use any CSS framework via `@import` in global.css (npm) or via a CDN
* link in `wrnexus.config.ts`'s `head` field.
*/
export type {
AppConfig,
BuildConfig,
ConfigIssue,
DevToolbarConfig,
ExplainedConfig,
ExperimentalConfig,
MobileConfig,
NavigationConfig,
ObservabilityConfig,
PerformanceConfig,
TenancyConfig,
PwaConfig,
StylesConfig,
StyleProcessContext,
Mode,
ResolvedConfigLayers,
} from "./config.ts";
export {
defineConfig,
explainAppConfig,
headToString,
loadAppConfig,
loadEnv,
loadRawConfig,
resolveProfile,
resolveConfigLayers,
validateAppConfig,
} from "./config.ts";
export {
CURRENT_COMPATIBILITY_DATE,
CURRENT_FRAMEWORK_BEHAVIOUR,
isCompatibilityDate,
resolveCompatibility,
} from "./compatibility.ts";
export type { CompatibilityPolicy, CompatibilityReport } from "./compatibility.ts";
export { findStyleEntry, bundleCss } from "./styles.ts";
export type { FontConfig, GoogleFont, LocalFontFace, FontDisplay } from "./fonts.ts";
export { renderFontHead, renderProductionFontHead, fontCspSources } from "./fonts.ts";
export type {
ThemeConfig,
ThemeTokens,
ResolvedTheme,
ThemePaletteName,
CustomThemePalette,
ThemeToken,
ThemeSemanticColor,
} from "./theme.ts";
export {
DEFAULT_THEMES,
THEME_PALETTES,
THEME_PALETTE_NAMES,
THEME_COOKIE,
THEME_CSS_HREF,
THEME_JS_HREF,
resolveThemeConfig,
resolveThemeName,
renderThemeCss,
renderThemeRuntime,
defineThemeTokens,
themeVar,
} from "./theme.ts";
import type { Mode, StyleProcessContext, StylesConfig } from "./config.ts";
import { bundleCss } from "./styles.ts";
import { mkdir, writeFile } from "node:fs/promises";
import { isAbsolute, join, relative } from "node:path";
/**
* Produce the final CSS for an entry: run the config's custom processor if one
* is provided (Tailwind/PostCSS/Sass), otherwise use the built-in Bun bundler.
*
* Development can fall back to best-effort CSS. Production throws by default so
* a deployment cannot silently ship unprocessed Tailwind/PostCSS directives.
*/
export async function renderStyles(
ctx: StyleProcessContext,
styles?: StylesConfig,
): Promise<string> {
if (!ctx.entryPath && !ctx.entries?.length) return "";
const processContext =
styles?.includePackageSources === false ? ctx : await packageAwareStyleContext(ctx);
if (!processContext.entryPath) return "";
if (styles?.process) {
try {
return String(await styles.process(processContext));
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
const details =
`[wrnexus] Stylesheet processing failed.\n` +
` ${msg.split("\n")[0]}\n` +
" If you use Tailwind, run `bun install` in the application workspace so the CLI and plugins are available.";
const failureMode = styles.failureMode ?? (ctx.mode === "production" ? "throw" : "fallback");
if (failureMode === "throw") throw new Error(details, { cause: err });
console.error(`\n${details}\n Serving best-effort unprocessed CSS in development.\n`);
return await fallbackCss(processContext);
}
}
return bundleCss(processContext.entryPath!, processContext.mode);
}
/**
* Best-effort CSS when a custom processor fails: try the built-in bundler; if that
* also fails (e.g. `@import "tailwindcss"` can't resolve), serve the raw entry with
* the tool-only directives stripped so the page still renders.
*/
async function fallbackCss(ctx: StyleProcessContext): Promise<string> {
try {
return await bundleCss(ctx.entryPath!, ctx.mode);
} catch {
try {
const raw = await Bun.file(ctx.entryPath!).text();
return raw.replace(/@import\s+["']tailwindcss["'];?/g, "").replace(/@source[^;\n]*;?/g, "");
} catch {
return "";
}
}
}
async function packageAwareStyleContext(ctx: StyleProcessContext): Promise<StyleProcessContext> {
const sources = [...new Set((ctx.sources ?? []).filter(Boolean))];
const entries = [...new Set((ctx.entries ?? []).filter(Boolean))];
if (sources.length === 0 && entries.length === 0) return ctx;
if (!ctx.entryPath && entries.length === 0) return ctx;
const outputDir = join(ctx.appRoot, ".wrnexus", "styles");
const wrapper = join(outputDir, "global.with-package-sources.css");
await mkdir(outputDir, { recursive: true });
const localPath = (value: string): string => {
const path = isAbsolute(value) ? relative(outputDir, value) : value;
const normalized = path.replace(/\\/g, "/");
return normalized.startsWith(".") ? normalized : `./${normalized}`;
};
const quote = (value: string) => localPath(value).replace(/"/g, '\\"');
const imports = [...(ctx.entryPath ? [ctx.entryPath] : []), ...entries]
.map((value) => `@import "${quote(value)}";`)
.join("\n");
const directives = sources.map((value) => `@source "${quote(value)}";`).join("\n");
await writeFile(
wrapper,
`${imports}\n\n/* WRNexus package scan sources */\n${directives}\n`,
"utf8",
);
return {
...ctx,
originalEntryPath: ctx.entryPath,
entryPath: wrapper,
sources,
entries,
};
}
export type { Mode as StylesMode };
export {
auditWireTokens,
normalizeStyleSources,
tailwindSourceDirectives,
contrast,
auditCssPerformance,
} from "./audit.ts";
export type {
CssTokenAudit,
StyleSource,
ContrastResult,
CssPerformanceAuditIssue,
} from "./audit.ts";
export * from "./theme.ts";