@nuxtwind/components
Version:
Component Library for Nuxt 3 using TailwindCSS
360 lines (351 loc) • 12.4 kB
JavaScript
import { useLogger, defineNuxtModule, createResolver, addVitePlugin, hasNuxtModule, installModule, addComponentsDir } from '@nuxt/kit';
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
import { join, dirname, relative, resolve } from 'node:path';
import { CSS_TEMPLATES, CSS_VALIDATION } from '../dist/runtime/css-templates.js';
function nxwLog(logActive, message, type = "info") {
if (logActive || type === "error" || type === "warn") {
const logger = useLogger("nuxtwind");
if (type === "info") logger.info(message);
if (type === "success") logger.success(message);
if (type === "warn") logger.warn(message);
if (type === "error") logger.error(message);
}
}
const name = "@nuxtwind/components";
const version = "2.1.1";
class CssManager {
nuxt;
options;
mainCssPath;
assetsDir;
constructor(nuxt, options = {}) {
this.nuxt = nuxt;
this.options = options;
const srcDir = nuxt.options.srcDir || nuxt.options.rootDir;
this.assetsDir = join(srcDir, "assets", "css");
this.mainCssPath = join(this.assetsDir, "main.css");
}
/**
* Ensures the main.css file exists and contains all required content
*/
ensureMainCssFile() {
try {
nxwLog(this.options.debugLog, `Checking for main.css file at: ${this.mainCssPath}`);
if (!existsSync(this.mainCssPath)) {
return this.createMainCssFile();
} else {
return this.validateAndUpdateMainCssFile();
}
} catch (error) {
nxwLog(this.options.debugLog, `Error managing main.css file: ${error}`, "error");
return false;
}
}
/**
* Creates a new main.css file with all required content
*/
createMainCssFile() {
nxwLog(this.options.debugLog, "main.css file not found, creating...", "warn");
if (!existsSync(this.assetsDir)) {
nxwLog(this.options.debugLog, `Creating directory: ${this.assetsDir}`);
mkdirSync(this.assetsDir, { recursive: true });
}
try {
let cssContent = CSS_TEMPLATES.complete();
cssContent = this.injectSourceDirectives(cssContent);
writeFileSync(this.mainCssPath, cssContent, "utf8");
nxwLog(this.options.debugLog, `Created main.css file at: ${this.mainCssPath}`, "success");
this.addToNuxtCssConfig();
return true;
} catch (error) {
nxwLog(this.options.debugLog, `Failed to create main.css file: ${error}`, "error");
return false;
}
}
/**
* Validates existing main.css file and updates it if necessary
*/
validateAndUpdateMainCssFile() {
nxwLog(this.options.debugLog, "main.css file already exists, validating content...");
try {
let content = readFileSync(this.mainCssPath, "utf8");
let hasChanges = false;
for (const section of CSS_VALIDATION.requiredSections) {
if (!section.pattern.test(content)) {
nxwLog(this.options.debugLog, `main.css missing ${section.name}, adding...`, "warn");
content = this.addMissingSection(content, section);
hasChanges = true;
} else {
nxwLog(this.options.debugLog, `main.css already contains ${section.name}`);
}
}
const withSources = this.injectSourceDirectives(content);
if (withSources !== content) {
content = withSources;
hasChanges = true;
}
if (hasChanges) {
writeFileSync(this.mainCssPath, content, "utf8");
nxwLog(this.options.debugLog, `Updated main.css file at: ${this.mainCssPath}`, "success");
}
this.addToNuxtCssConfig();
return true;
} catch (error) {
nxwLog(this.options.debugLog, `Failed to validate/update main.css file: ${error}`, "error");
return false;
}
}
/**
* Adds missing section to CSS content in the appropriate position
*/
addMissingSection(content, section) {
switch (section.position) {
case "top":
return `${section.content}
${content}`;
case "bottom":
return `${content}
${section.content}`;
case "middle":
default: {
const lines = content.split("\n");
const lastImportIndex = this.findLastImportIndex(lines);
const insertIndex = lastImportIndex + 1;
lines.splice(insertIndex, 0, "", section.content, "");
return lines.join("\n");
}
}
}
/**
* Finds the index of the last import statement
*/
findLastImportIndex(lines) {
let lastImportIndex = -1;
for (let i = 0; i < lines.length; i++) {
if (lines[i]?.trim().startsWith("@import")) {
lastImportIndex = i;
}
}
return lastImportIndex;
}
/**
* Adds the CSS file to Nuxt's CSS configuration if not already present
*/
addToNuxtCssConfig() {
const cssPath = "~/assets/css/main.css";
if (!this.nuxt.options.css.includes(cssPath)) {
this.nuxt.options.css.push(cssPath);
nxwLog(this.options.debugLog, `Added ${cssPath} to Nuxt CSS configuration`, "success");
}
}
/**
* Injects @source directives for any configured external sources, if missing.
* Places them right after the Tailwind import for clarity.
*/
injectSourceDirectives(content) {
const sources = this.options.externalSources || [];
if (!sources.length) return content;
const cssDir = dirname(this.mainCssPath);
const existing = new Set(
Array.from(content.matchAll(/@source\s+"([^"]+)"\s*;?/g)).map((m) => m[1])
);
const neededLines = [];
for (const absPath of sources) {
try {
const rel = relative(cssDir, absPath) || ".";
const relPosix = rel.split("\\").join("/");
if (!existing.has(relPosix)) {
neededLines.push(`@source "${relPosix}";`);
}
} catch (e) {
nxwLog(this.options.debugLog, `Failed to compute @source path for ${absPath}: ${e}`, "warn");
}
}
if (!neededLines.length) return content;
const lines = content.split("\n");
const lastImportIndex = this.findLastImportIndex(lines);
const insertIndex = lastImportIndex >= 0 ? lastImportIndex + 1 : 0;
lines.splice(insertIndex, 0, "", ...neededLines, "");
return lines.join("\n");
}
/**
* Validates if the current CSS file has all required content
*/
validateCssFile() {
if (!existsSync(this.mainCssPath)) {
return { valid: false, missing: ["File does not exist"] };
}
try {
const content = readFileSync(this.mainCssPath, "utf8");
const missing = [];
for (const section of CSS_VALIDATION.requiredSections) {
if (!section.pattern.test(content)) {
missing.push(section.name);
}
}
return { valid: missing.length === 0, missing };
} catch (error) {
return { valid: false, missing: [`Error reading file: ${error}`] };
}
}
}
class ConfigLoader {
nuxt;
options;
constructor(nuxt, options = {}) {
this.nuxt = nuxt;
this.options = options;
}
async loadUserConfig(customConfigPath) {
try {
const rootDir = this.nuxt.options.rootDir;
let configPath = null;
if (customConfigPath) {
nxwLog(this.options.debugLog, `Looking for custom config file at: ${customConfigPath}`);
const resolvedPath = resolve(rootDir, customConfigPath);
if (existsSync(resolvedPath)) {
configPath = resolvedPath;
nxwLog(this.options.debugLog, `Found custom config file: ${customConfigPath}`, "success");
} else {
nxwLog(true, `Custom config file not found at: ${customConfigPath}`, "error");
return null;
}
} else {
nxwLog(this.options.debugLog, "Looking for nuxtwind.config file...");
const configExtensions = ["ts", "js", "mjs"];
for (const ext of configExtensions) {
const path = resolve(rootDir, `nuxtwind.config.${ext}`);
if (existsSync(path)) {
configPath = path;
nxwLog(this.options.debugLog, `Found nuxtwind.config.${ext} file`, "success");
break;
}
}
if (!configPath) {
nxwLog(this.options.debugLog, "No nuxtwind.config file found, using default configurations");
return null;
}
}
const configModule = await import(configPath);
const config = configModule.default || configModule;
if (config && typeof config === "object" && Object.keys(config).length > 0) {
nxwLog(this.options.debugLog, `Successfully loaded config`);
return config;
} else {
nxwLog(this.options.debugLog, "Config file found but no valid configuration exported");
return null;
}
} catch (error) {
nxwLog(true, `Failed to load config file: ${error}`, "error");
return null;
}
}
}
function manageCssFile(nuxt, options, runtimeDir) {
const componentsDir = join(runtimeDir, "components");
const cssManager = new CssManager(nuxt, {
debugLog: options.debugLog,
externalSources: [componentsDir]
});
if (options.css?.autoUpdate !== false) {
const success = cssManager.validateAndUpdateMainCssFile();
if (!success) {
nxwLog(options.debugLog, "Failed to validate or update main.css file", "error");
}
}
if (options.css?.autoCreate !== false) {
const success = cssManager.ensureMainCssFile();
if (!success) {
nxwLog(true, "Failed to manage main.css file", "error");
}
}
}
async function loadAndProvideUserConfig(nuxt, options) {
const configLoader = new ConfigLoader(nuxt, {
debugLog: options.debugLog
});
return await configLoader.loadUserConfig(options.configPath);
}
const module$1 = defineNuxtModule({
meta: {
name,
version,
configKey: "nuxtwind",
compatibility: {
nuxt: "^4.0.0"
}
},
// Default configuration options of the Nuxt module
defaults: {
prefix: "NXW-",
global: false,
debugLog: false,
css: {
autoCreate: true,
autoUpdate: true
},
configPath: void 0
},
async setup(_options, _nuxt) {
nxwLog(_options.debugLog, "Setting up NuxtWind Module");
const resolver = createResolver(import.meta.url);
const runtimeDir = resolver.resolve("./runtime");
_nuxt.options.build.transpile.push(runtimeDir);
_nuxt.options.alias["#nuxtwind"] = runtimeDir;
const runtimeCss = "#nuxtwind/global.css";
if (!_nuxt.options.css.includes(runtimeCss)) {
_nuxt.options.css.push(runtimeCss);
nxwLog(_options.debugLog, `Added ${runtimeCss} to Nuxt CSS for Tailwind @source`, "success");
}
const userConfig = await loadAndProvideUserConfig(_nuxt, _options);
if (userConfig) {
nxwLog(_options.debugLog, "User configuration has been loaded:", "info");
nxwLog(_options.debugLog, JSON.stringify(userConfig, null, 2));
_nuxt.options.runtimeConfig.public.nuxtwind = userConfig;
} else {
nxwLog(_options.debugLog, "No user configuration found, using default options", "warn");
_nuxt.options.runtimeConfig.public.nuxtwind = {};
}
manageCssFile(_nuxt, _options, runtimeDir);
nxwLog(_options.debugLog, "Installing tailwindcss v4 standalone");
const tailwindPlugin = await import('@tailwindcss/vite').then(
(r) => r.default
);
addVitePlugin(tailwindPlugin);
nxwLog(
_options.debugLog,
"Checking for already installed @nuxtjs/color-mode module"
);
if (hasNuxtModule("@nuxtjs/color-mode")) {
nxwLog(
_options.debugLog,
"@nuxtjs/color-mode module is already installed, skipping installation"
);
} else {
nxwLog(
_options.debugLog,
"@nuxtjs/color-mode module not found, proceeding with installation",
"warn"
);
if (!_options.colorMode) {
_options.colorMode = { classSuffix: "" };
}
if (!_options.colorMode.classSuffix) {
_options.colorMode.classSuffix = "";
}
nxwLog(
_options.debugLog,
`Using color mode class suffix: ${_options.colorMode.classSuffix}`
);
await installModule("@nuxtjs/color-mode", _options.colorMode);
}
nxwLog(_options.debugLog, "Adding components directory");
addComponentsDir({
prefix: _options.prefix,
path: resolver.resolve(runtimeDir, "components"),
global: _options.global
});
nxwLog(true, "NuxtWind Module setup complete", "success");
}
});
export { module$1 as default };