UNPKG

@nuxtwind/components

Version:

Component Library for Nuxt 3 using TailwindCSS

360 lines (351 loc) 12.4 kB
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 };