613 lines
31 KiB
JavaScript
613 lines
31 KiB
JavaScript
import { relative, join, dirname, isAbsolute, resolve } from 'pathe';
|
|
import { resolvePath, tryResolveModule, useNuxt, useLogger, addTemplate, addTypeTemplate, isNuxtMajorVersion, addDevServerHandler, updateTemplates, findPath, resolveAlias, defineNuxtModule, getNuxtVersion, installModule, addImports, createResolver, addVitePlugin } from '@nuxt/kit';
|
|
import { readPackageJSON } from 'pkg-types';
|
|
import { existsSync } from 'node:fs';
|
|
import { defu } from 'defu';
|
|
import { LogLevels } from 'consola';
|
|
import { getContext } from 'unctx';
|
|
import { colors } from 'consola/utils';
|
|
import { eventHandler, sendRedirect, H3Event } from 'h3';
|
|
import { joinURL, withTrailingSlash, withoutTrailingSlash, cleanDoubleSlashes } from 'ufo';
|
|
import { loadConfig as loadConfig$1 } from 'c12';
|
|
import configMerger from './merger.mjs';
|
|
import { diff } from 'ohash/utils';
|
|
import 'klona';
|
|
|
|
const name = "@nuxtjs/tailwindcss";
|
|
const version = "6.14.0";
|
|
const configKey = "tailwindcss";
|
|
const compatibility = {
|
|
nuxt: "^2.9.0 || >=3.0.0-rc.1"
|
|
};
|
|
|
|
async function resolveCSSPath(cssPath, nuxt = useNuxt()) {
|
|
if (typeof cssPath === "string") {
|
|
const _cssPath = await resolvePath(cssPath, { extensions: [".css", ".sass", ".scss", ".less", ".styl"] });
|
|
return existsSync(_cssPath) ? [_cssPath, `Using Tailwind CSS from ~/${relative(nuxt.options.srcDir, _cssPath)}`] : await tryResolveModule("tailwindcss/package.json", import.meta.url).then((twLocation) => twLocation ? [join(twLocation, "../tailwind.css"), "Using default Tailwind CSS file"] : Promise.reject("Unable to resolve tailwindcss. Is it installed?"));
|
|
} else {
|
|
return [
|
|
false,
|
|
"No Tailwind CSS file found. Skipping..."
|
|
];
|
|
}
|
|
}
|
|
const resolveBoolObj = (config, fb) => defu(typeof config === "object" ? config : {}, fb);
|
|
const resolveViewerConfig = (config) => resolveBoolObj(config, { endpoint: "/_tailwind", exportViewer: false });
|
|
const resolveExposeConfig = (config) => resolveBoolObj(config, { alias: "#tailwind-config", level: 2 });
|
|
const resolveEditorSupportConfig = (config) => resolveBoolObj(config, { autocompleteUtil: true, generateConfig: false });
|
|
async function resolveInjectPosition(css, position = "first") {
|
|
if (typeof position === "number") {
|
|
return ~~Math.min(position, css.length + 1);
|
|
}
|
|
if (typeof position === "string") {
|
|
switch (position) {
|
|
case "first":
|
|
return 0;
|
|
case "last":
|
|
return css.length;
|
|
}
|
|
}
|
|
if (typeof position === "object") {
|
|
const minIndex = "after" in position ? css.indexOf(await resolvePath(position.after)) + 1 : 0;
|
|
const maxIndex = "before" in position ? css.indexOf(await resolvePath(position.before)) : css.length;
|
|
if ([minIndex, maxIndex].includes(-1) || "after" in position && minIndex === 0) {
|
|
throw new Error(`\`injectPosition\` specifies a file which does not exists on CSS stack: ` + JSON.stringify(position));
|
|
}
|
|
if (minIndex > maxIndex) {
|
|
throw new Error(`\`injectPosition\` specifies a relative location \`${minIndex}\` that cannot be resolved (i.e., \`after\` orders \`before\` may be reversed): ` + JSON.stringify(position));
|
|
}
|
|
return "after" in position ? minIndex : maxIndex;
|
|
}
|
|
throw new Error("invalid `injectPosition`: " + JSON.stringify(position));
|
|
}
|
|
|
|
const logger = useLogger("nuxt:tailwindcss");
|
|
|
|
const twCtx = getContext("twcss");
|
|
const { set } = twCtx;
|
|
twCtx.set = (instance, replace = true) => {
|
|
set(defu(instance, twCtx.tryUse()), replace);
|
|
};
|
|
|
|
const NON_ALPHANUMERIC_RE = /^[0-9a-z]+$/i;
|
|
const isJSObject = (value) => typeof value === "object" && !Array.isArray(value);
|
|
const createExposeTemplates = (config, nuxt = useNuxt()) => {
|
|
const templates = [];
|
|
const getTWConfig = (objPath = [], twConfig = twCtx.use().config) => objPath.reduce((prev, curr) => prev?.[curr], twConfig);
|
|
const populateMap = (obj = twCtx.use().config, path = [], level = 1) => {
|
|
Object.entries(obj).forEach(([key, value = {}]) => {
|
|
const subpathComponents = path.concat(key);
|
|
const subpath = subpathComponents.join("/");
|
|
if (level >= config.level || !isJSObject(value) || Object.keys(value).find((k) => !k.match(NON_ALPHANUMERIC_RE))) {
|
|
templates.push(addTemplate({
|
|
filename: `tailwind/expose/${subpath}.mjs`,
|
|
getContents: () => {
|
|
const _value = getTWConfig(subpathComponents);
|
|
if (isJSObject(_value)) {
|
|
const [validKeys, invalidKeys] = [[], []];
|
|
Object.keys(_value).forEach((i) => (NON_ALPHANUMERIC_RE.test(i) ? validKeys : invalidKeys).push(i));
|
|
return [
|
|
`${validKeys.map((i) => `const _${i} = ${JSON.stringify(_value[i])}`).join("\n")}`,
|
|
`const config = { ${validKeys.map((i) => `"${i}": _${i}, `).join("")}${invalidKeys.map((i) => `"${i}": ${JSON.stringify(_value[i])}, `).join("")} }`,
|
|
`export { config as default${validKeys.length > 0 ? ", _" : ""}${validKeys.join(", _")} }`
|
|
].join("\n");
|
|
}
|
|
return `export default ${JSON.stringify(_value, null, 2)}`;
|
|
},
|
|
write: config.write
|
|
}));
|
|
} else {
|
|
populateMap(value, path.concat(key), level + 1);
|
|
templates.push(addTemplate({
|
|
filename: `tailwind/expose/${subpath}.mjs`,
|
|
getContents: () => {
|
|
const _value = getTWConfig(subpathComponents);
|
|
const values = Object.keys(_value);
|
|
return [
|
|
`${values.map((v) => `import _${v} from "./${key}/${v}.mjs"`).join("\n")}`,
|
|
`const config = { ${values.map((k) => `"${k}": _${k}`).join(", ")} }`,
|
|
`export { config as default${values.length > 0 ? ", _" : ""}${values.join(", _")} }`
|
|
].join("\n");
|
|
},
|
|
write: config.write
|
|
}));
|
|
}
|
|
});
|
|
};
|
|
populateMap();
|
|
const entryTemplate = addTemplate({
|
|
filename: "tailwind/expose/index.mjs",
|
|
getContents: () => {
|
|
const _tailwindConfig = getTWConfig();
|
|
const configOptions = Object.keys(_tailwindConfig);
|
|
return [
|
|
`${configOptions.map((v) => `import ${v} from "#build/tailwind/expose/${v}.mjs"`).join("\n")}`,
|
|
`const config = { ${configOptions.join(", ")} }`,
|
|
`export { config as default, ${configOptions.join(", ")} }`
|
|
].join("\n");
|
|
},
|
|
write: true
|
|
});
|
|
templates.push(addTypeTemplate({
|
|
filename: "types/tailwind.config.d.ts",
|
|
getContents: () => {
|
|
const _tailwindConfig = getTWConfig();
|
|
const declareModule = (obj, path = [], level = 1) => Object.entries(obj).map(([key, value = {}]) => {
|
|
const subpath = path.concat(key).join("/");
|
|
if (level >= config.level || !isJSObject(value) || Object.keys(value).find((k) => !k.match(NON_ALPHANUMERIC_RE))) {
|
|
if (isJSObject(value)) {
|
|
const [validKeys, invalidKeys] = [[], []];
|
|
Object.keys(value).forEach((i) => (NON_ALPHANUMERIC_RE.test(i) ? validKeys : invalidKeys).push(i));
|
|
return `declare module "${config.alias}/${subpath}" { ${validKeys.map((i) => `export const _${i}: ${JSON.stringify(value[i])};`).join("")} const defaultExport: { ${validKeys.map((i) => `"${i}": typeof _${i}, `).join("")}${invalidKeys.map((i) => `"${i}": ${JSON.stringify(value[i])}, `).join("")} }; export default defaultExport; }
|
|
`;
|
|
}
|
|
return `declare module "${config.alias}/${subpath}" { const defaultExport: ${JSON.stringify(value)}; export default defaultExport; }
|
|
`;
|
|
}
|
|
const values = Object.keys(value);
|
|
return declareModule(value, path.concat(key), level + 1).join("") + `declare module "${config.alias}/${subpath}" {${Object.keys(value).map((v) => ` export const _${v}: typeof import("${config.alias}/${join(`${key}/${subpath}`, `../${v}`)}")["default"];`).join("")} const defaultExport: { ${values.map((k) => `"${k}": typeof _${k}`).join(", ")} }; export default defaultExport; }
|
|
`;
|
|
});
|
|
const configOptions = Object.keys(_tailwindConfig);
|
|
return declareModule(_tailwindConfig).join("") + `declare module "${config.alias}" {${configOptions.map((v) => ` export const ${v}: typeof import("${join(config.alias, v)}")["default"];`).join("")} const defaultExport: { ${configOptions.map((v) => `"${v}": typeof ${v}`)} }; export default defaultExport; }`;
|
|
}
|
|
}));
|
|
templates.push(entryTemplate);
|
|
nuxt.options.alias[config.alias] = dirname(entryTemplate.dst);
|
|
return templates.map((t) => t.dst);
|
|
};
|
|
|
|
const setupViewer = async (twConfig, config, nuxt = useNuxt()) => {
|
|
const route = joinURL(nuxt.options.app?.baseURL, config.endpoint);
|
|
const [routeWithSlash, routeWithoutSlash] = [withTrailingSlash(route), withoutTrailingSlash(route)];
|
|
const viewerServer = await Promise.all([
|
|
// @ts-expect-error untyped package export
|
|
import('tailwind-config-viewer/server/index.js').then((r) => r.default || r),
|
|
typeof twConfig === "string" ? import('tailwindcss/loadConfig.js').then((r) => r.default || r).then((loadConfig) => () => loadConfig(twConfig)) : () => twConfig
|
|
]).then(([server, tailwindConfigProvider]) => server({ tailwindConfigProvider }).asMiddleware());
|
|
const viewerDevMiddleware = eventHandler((event) => viewerServer(event.node?.req || event.req, event.node?.res || event.res));
|
|
if (!isNuxtMajorVersion(2, nuxt)) {
|
|
addDevServerHandler({
|
|
handler: eventHandler((event) => {
|
|
if (event.path === routeWithoutSlash) {
|
|
return sendRedirect(event, routeWithSlash, 301);
|
|
}
|
|
})
|
|
});
|
|
addDevServerHandler({ route, handler: viewerDevMiddleware });
|
|
} else {
|
|
nuxt.options.serverMiddleware.push(
|
|
// @ts-expect-error untyped handler parameters
|
|
(req, res, next) => {
|
|
if (req.url === routeWithoutSlash) {
|
|
return sendRedirect(new H3Event(req, res), routeWithSlash, 301);
|
|
}
|
|
next();
|
|
},
|
|
// @ts-expect-error untyped handler parameters
|
|
{ route, handler: (req, res) => viewerDevMiddleware(new H3Event(req, res)) }
|
|
);
|
|
}
|
|
nuxt.hook("devtools:customTabs", (tabs) => {
|
|
tabs?.push({
|
|
title: "Tailwind CSS",
|
|
name: "tailwindcss",
|
|
icon: "logos-tailwindcss-icon",
|
|
category: "modules",
|
|
view: { type: "iframe", src: route }
|
|
});
|
|
});
|
|
const shouldLogUrl = "devtools" in nuxt.options ? !nuxt.options.devtools.enabled : true;
|
|
shouldLogUrl && nuxt.hook("listen", (_, listener) => {
|
|
const viewerUrl = cleanDoubleSlashes(joinURL(listener.url, config.endpoint));
|
|
logger.info(`Tailwind Viewer: ${colors.underline(colors.yellow(withTrailingSlash(viewerUrl)))}`);
|
|
});
|
|
};
|
|
const exportViewer = async (twConfig, config, nuxt = useNuxt()) => {
|
|
if (!config.exportViewer) {
|
|
return;
|
|
}
|
|
const cli = await import('tailwind-config-viewer/cli/export.js').then((r) => r.default || r);
|
|
nuxt.hook("nitro:build:public-assets", (nitro) => {
|
|
const dir = joinURL(nitro.options.output.publicDir, config.endpoint);
|
|
cli(dir, twConfig);
|
|
logger.success(`Exported viewer to ${colors.yellow(relative(nuxt.options.srcDir, dir))}`);
|
|
});
|
|
};
|
|
|
|
const checkUnsafeInlineConfig = (inlineConfig) => {
|
|
if (!inlineConfig)
|
|
return;
|
|
if ("plugins" in inlineConfig && Array.isArray(inlineConfig.plugins) && inlineConfig.plugins.find((p) => typeof p === "function" || typeof p?.handler === "function")) {
|
|
return "plugins";
|
|
}
|
|
if (inlineConfig.content) {
|
|
const invalidProperty = ["extract", "transform"].find((i) => i in inlineConfig.content && typeof inlineConfig.content[i] === "function");
|
|
if (invalidProperty) {
|
|
return `content.${invalidProperty}`;
|
|
}
|
|
}
|
|
if (inlineConfig.safelist) {
|
|
const invalidIdx = inlineConfig.safelist.findIndex((s) => typeof s === "object" && s.pattern instanceof RegExp);
|
|
if (invalidIdx > -1) {
|
|
return `safelist[${invalidIdx}]`;
|
|
}
|
|
}
|
|
};
|
|
|
|
const UNSUPPORTED_VAL_STR = "UNSUPPORTED_VAL_STR";
|
|
const JSONStringifyWithUnsupportedVals = (val) => JSON.stringify(val, (_, v) => ["function"].includes(typeof v) ? UNSUPPORTED_VAL_STR : v);
|
|
const JSONStringifyWithRegex = (obj) => JSON.stringify(obj, (_, v) => v instanceof RegExp ? `__REGEXP ${v.toString()}` : v);
|
|
const createObjProxy = (configUpdatedHook, meta) => {
|
|
return (configPath, oldConfig, newConfig) => diff(oldConfig, newConfig).forEach((change) => {
|
|
const path = change.key.split(".").map((k) => `[${JSON.stringify(k)}]`).join("");
|
|
const newValue = change.newValue?.value;
|
|
switch (change.type) {
|
|
case "removed":
|
|
configUpdatedHook[configPath] += `delete cfg${path};`;
|
|
break;
|
|
case "added":
|
|
case "changed": {
|
|
const resultingCode = `cfg${path} = ${JSONStringifyWithRegex(newValue)?.replace(/"__REGEXP (.*)"/g, (_, substr) => substr.replace(/\\"/g, '"')) || `cfg${path}`};`;
|
|
if (JSONStringifyWithUnsupportedVals(change.oldValue?.value) === JSONStringifyWithUnsupportedVals(newValue) || configUpdatedHook[configPath].endsWith(resultingCode)) {
|
|
return;
|
|
}
|
|
if (JSONStringifyWithUnsupportedVals(newValue).includes(`"${UNSUPPORTED_VAL_STR}"`) && !meta?.disableHMR) {
|
|
logger.warn(
|
|
`A hook has injected a non-serializable value in \`config${path}\`, so the Tailwind Config cannot be serialized. Falling back to providing the loaded configuration inlined directly to PostCSS loader..`,
|
|
"Please consider using a configuration file/template instead (specifying in `configPath` of the module options) to enable additional support for IntelliSense and HMR."
|
|
);
|
|
twCtx.set({ meta: { disableHMR: true } });
|
|
}
|
|
if (JSONStringifyWithRegex(newValue).includes("__REGEXP") && !meta?.disableHMR) {
|
|
logger.warn(`A hook is injecting RegExp values in your configuration (check \`config${path}\`) which may be unsafely serialized. Consider moving your safelist to a separate configuration file/template instead (specifying in \`configPath\` of the module options)`);
|
|
}
|
|
configUpdatedHook[configPath] += resultingCode;
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
const loadConfig = loadConfig$1;
|
|
const pagesContentPath = getContext("twcss-pages-path");
|
|
const componentsContentPath = getContext("twcss-components-path");
|
|
const resolvedConfigsCtx = getContext("twcss-resolved-configs");
|
|
const createInternalContext = async (moduleOptions, nuxt = useNuxt()) => {
|
|
const configUpdatedHook = {};
|
|
const { meta = { disableHMR: moduleOptions.disableHMR } } = twCtx.tryUse() ?? {};
|
|
const trackObjChanges = createObjProxy(configUpdatedHook, meta);
|
|
const resolveConfigs = (configs, nuxt2 = useNuxt()) => (Array.isArray(configs) ? configs : [configs]).filter((c) => Boolean(c) && c !== join(nuxt2.options.rootDir, "tailwind.config")).map(async (config, idx, arr) => {
|
|
if (typeof config !== "string") {
|
|
const hasUnsafeProperty = checkUnsafeInlineConfig(config);
|
|
if (hasUnsafeProperty && !meta.disableHMR) {
|
|
logger.warn(
|
|
`The provided Tailwind configuration in your \`nuxt.config\` is non-serializable. Check \`${hasUnsafeProperty}\`. Falling back to providing the loaded configuration inlined directly to PostCSS loader..`,
|
|
"Please consider using `tailwind.config` or a separate file (specifying in `configPath` of the module options) to enable it with additional support for IntelliSense and HMR. Suppress this warning with `quiet: true` in the module options."
|
|
);
|
|
meta.disableHMR = true;
|
|
twCtx.set({ meta });
|
|
}
|
|
return { config };
|
|
}
|
|
const configFile = await (config.startsWith(nuxt2.options.buildDir) ? config : findPath(config, { extensions: [".js", ".cjs", ".mjs", ".ts"] }));
|
|
return configFile ? loadConfig({ configFile }).then(async (resolvedConfig) => {
|
|
const { configFile: resolvedConfigFile = configFile } = resolvedConfig;
|
|
const config2 = configMerger(void 0, resolvedConfig.config);
|
|
configUpdatedHook[resolvedConfigFile] = "";
|
|
if (resolvedConfig.config?.purge && !resolvedConfig.config.content) {
|
|
configUpdatedHook[resolvedConfigFile] += "cfg.content = cfg.purge;";
|
|
}
|
|
await nuxt2.callHook("tailwindcss:loadConfig", config2, resolvedConfigFile, idx, arr);
|
|
trackObjChanges(resolvedConfigFile, resolvedConfig.config, config2);
|
|
return { ...resolvedConfig, config: config2 };
|
|
}).catch((e) => {
|
|
logger.warn(`Failed to load config \`./${relative(nuxt2.options.rootDir, configFile)}\` due to the error below. Skipping..
|
|
`, e);
|
|
return null;
|
|
}) : null;
|
|
});
|
|
const resolveContentConfig = (rootDir, nuxtOptions = useNuxt().options) => {
|
|
const r = (p) => isAbsolute(p) || p.startsWith(rootDir) ? p : resolve(rootDir, p);
|
|
const withSrcDir = (p) => r(nuxtOptions.srcDir && !p.startsWith(nuxtOptions.srcDir) ? resolve(nuxtOptions.srcDir, p) : p);
|
|
const formatExtensions = (s) => s.length > 1 ? `.{${s.join(",")}}` : `.${s.join("") || "vue"}`;
|
|
const defaultExtensions = formatExtensions(["js", "ts", "mjs"]);
|
|
const sfcExtensions = formatExtensions(Array.from(/* @__PURE__ */ new Set([".vue", ...nuxtOptions.extensions || nuxt.options.extensions])).map((e) => e?.replace(/^\.*/, "")).filter((v) => Boolean(v)));
|
|
const importDirs = [...nuxtOptions.imports?.dirs || []].filter((v) => Boolean(v)).map(withSrcDir);
|
|
const [composablesDir, utilsDir] = [withSrcDir("composables"), withSrcDir("utils")];
|
|
if (!importDirs.includes(composablesDir))
|
|
importDirs.push(composablesDir);
|
|
if (!importDirs.includes(utilsDir))
|
|
importDirs.push(utilsDir);
|
|
const isLayer = rootDir !== nuxt.options.rootDir;
|
|
const pagePaths = [];
|
|
const pageFiles = pagesContentPath.tryUse();
|
|
if (moduleOptions.experimental?.strictScanContentPaths && pageFiles && pageFiles.length) {
|
|
if (!isLayer)
|
|
pagePaths.push(...pageFiles.map((p) => p.replaceAll(/\[(\.+)([^.].*)\]/g, "?$1$2?")));
|
|
} else if (nuxtOptions.pages !== false && nuxtOptions.pages?.enabled !== false) {
|
|
pagePaths.push(withSrcDir(`${nuxtOptions.dir?.pages || "pages"}/**/*${sfcExtensions}`));
|
|
}
|
|
const componentPaths = [];
|
|
const componentFiles = componentsContentPath.tryUse();
|
|
if (moduleOptions.experimental?.strictScanContentPaths && componentFiles && componentFiles.length) {
|
|
if (!isLayer)
|
|
componentPaths.push(...componentFiles);
|
|
} else {
|
|
componentPaths.push(
|
|
withSrcDir(`components/**/*${sfcExtensions}`),
|
|
...(() => {
|
|
if (nuxtOptions.components) {
|
|
return (Array.isArray(nuxtOptions.components) ? nuxtOptions.components : typeof nuxtOptions.components === "boolean" ? ["components"] : nuxtOptions.components.dirs || []).map((d) => {
|
|
const valueToResolve = typeof d === "string" ? d : d?.path;
|
|
return valueToResolve ? `${resolveAlias(valueToResolve)}/**/*${sfcExtensions}` : "";
|
|
}).filter(Boolean);
|
|
}
|
|
return [];
|
|
})()
|
|
);
|
|
}
|
|
return {
|
|
config: {
|
|
content: {
|
|
files: [
|
|
...componentPaths,
|
|
nuxtOptions.dir?.layouts && withSrcDir(`${nuxtOptions.dir.layouts}/**/*${sfcExtensions}`),
|
|
nuxtOptions.dir?.plugins && withSrcDir(`${nuxtOptions.dir.plugins}/**/*${defaultExtensions}`),
|
|
...importDirs.map((d) => `${d}/**/*${defaultExtensions}`),
|
|
...pagePaths,
|
|
withSrcDir(`{A,a}pp${sfcExtensions}`),
|
|
withSrcDir(`{E,e}rror${sfcExtensions}`),
|
|
withSrcDir(`app.config${defaultExtensions}`),
|
|
!nuxtOptions.ssr && nuxtOptions.spaLoadingTemplate !== false && r(typeof nuxtOptions.spaLoadingTemplate === "string" ? nuxtOptions.spaLoadingTemplate : "app/spa-loading-template.html")
|
|
].filter((p) => Boolean(p))
|
|
}
|
|
}
|
|
};
|
|
};
|
|
const resolvePageFiles = (pages) => {
|
|
const filePaths = [];
|
|
pages.forEach((page) => {
|
|
if (page.file) {
|
|
filePaths.push(page.file);
|
|
}
|
|
if (page.children && page.children.length) {
|
|
filePaths.push(...resolvePageFiles(page.children));
|
|
}
|
|
});
|
|
return filePaths;
|
|
};
|
|
const getModuleConfigs = () => {
|
|
const thenCallHook = async (resolvedConfig) => {
|
|
const { configFile: resolvedConfigFile } = resolvedConfig;
|
|
if (!resolvedConfigFile || !resolvedConfig.config) {
|
|
return { ...resolvedConfig, configFile: resolvedConfigFile === "tailwind.config" ? void 0 : resolvedConfigFile };
|
|
}
|
|
const config = configMerger(void 0, resolvedConfig.config);
|
|
configUpdatedHook[resolvedConfigFile] = "";
|
|
if (resolvedConfig.config?.purge && !resolvedConfig.config.content) {
|
|
configUpdatedHook[resolvedConfigFile] += "cfg.content = cfg.purge;";
|
|
}
|
|
await nuxt.callHook("tailwindcss:loadConfig", config, resolvedConfigFile, 0, []);
|
|
trackObjChanges(resolvedConfigFile, resolvedConfig.config, config);
|
|
return { ...resolvedConfig, config };
|
|
};
|
|
return Promise.all([
|
|
resolveContentConfig(nuxt.options.rootDir, nuxt.options),
|
|
...resolveConfigs(moduleOptions.config, nuxt),
|
|
loadConfig({ name: "tailwind", cwd: nuxt.options.rootDir, merger: configMerger, packageJson: true, extend: false }).then(thenCallHook),
|
|
...resolveConfigs(moduleOptions.configPath, nuxt),
|
|
...(nuxt.options._layers || []).slice(1).flatMap((nuxtLayer) => [
|
|
resolveContentConfig(nuxtLayer.config.rootDir || nuxtLayer.cwd, nuxtLayer.config),
|
|
...resolveConfigs(nuxtLayer.config.tailwindcss?.config, nuxt),
|
|
loadConfig({ name: "tailwind", cwd: nuxtLayer.cwd, merger: configMerger, packageJson: true, extend: false }).then(thenCallHook),
|
|
...resolveConfigs(nuxtLayer.config.tailwindcss?.configPath, nuxt)
|
|
])
|
|
]);
|
|
};
|
|
const resolveTWConfig = await import('tailwindcss/resolveConfig.js').then((m) => m.default || m).catch(() => (c) => c);
|
|
const loadConfigs = async () => {
|
|
const moduleConfigs = await getModuleConfigs();
|
|
resolvedConfigsCtx.set(moduleConfigs, true);
|
|
const tailwindConfig = moduleConfigs.reduce((acc, curr) => configMerger(acc, curr?.config ?? {}), {});
|
|
const clonedConfig = configMerger(void 0, tailwindConfig);
|
|
configUpdatedHook["main-config"] = "";
|
|
await nuxt.callHook("tailwindcss:config", clonedConfig);
|
|
trackObjChanges("main-config", tailwindConfig, clonedConfig);
|
|
const resolvedConfig = resolveTWConfig(clonedConfig);
|
|
await nuxt.callHook("tailwindcss:resolvedConfig", resolvedConfig, twCtx.tryUse()?.config ?? void 0);
|
|
twCtx.set({ config: resolvedConfig });
|
|
return tailwindConfig;
|
|
};
|
|
const generateConfig = () => {
|
|
const ctx = twCtx.tryUse();
|
|
const targetDir = join(nuxt.options.buildDir, "tailwind");
|
|
const template = !meta.disableHMR || !ctx?.meta?.disableHMR ? addTemplate({
|
|
filename: "tailwind/postcss.mjs",
|
|
write: true,
|
|
getContents: () => {
|
|
const serializeConfig = (config) => JSON.stringify(
|
|
Array.isArray(config.plugins) && config.plugins.length > 0 ? configMerger({ plugins: (defaultPlugins) => defaultPlugins?.filter((p) => p && typeof p !== "function") }, config) : config,
|
|
(_, v) => typeof v === "function" ? `() => (${JSON.stringify(v())})` : v
|
|
).replace(/"(\(\) => \(.*\))"/g, (_, substr) => substr.replace(/\\"/g, '"'));
|
|
const layerConfigs = resolvedConfigsCtx.use().map((c, idx) => c?.configFile ? [`import cfg${idx} from ${JSON.stringify(/[/\\]node_modules[/\\]/.test(c.configFile) ? c.configFile : "./" + relative(targetDir, c.configFile))}`, configUpdatedHook[c.configFile] ? `(() => {const cfg=configMerger(undefined, cfg${idx});${configUpdatedHook[c.configFile]};return cfg;})()` : `cfg${idx}`] : [null, c?.config ? serializeConfig(c.config) : null]);
|
|
return [
|
|
`// generated by the @nuxtjs/tailwindcss <https://github.com/nuxt-modules/tailwindcss> module at ${( new Date()).toLocaleString()}`,
|
|
'import "@nuxtjs/tailwindcss/config-ctx"',
|
|
`import configMerger from "@nuxtjs/tailwindcss/merger";
|
|
`,
|
|
layerConfigs.map(([i, _]) => i).filter(Boolean).join(";\n") + ";",
|
|
"const config = [",
|
|
layerConfigs.map(([_, i]) => i).filter(Boolean).join(",\n"),
|
|
`].reduce((acc, curr) => configMerger(acc, curr), {});
|
|
`,
|
|
`const resolvedConfig = ${configUpdatedHook["main-config"] ? `(() => {const cfg=config;${configUpdatedHook["main-config"]};return cfg;})()` : "config"};
|
|
`,
|
|
"export default resolvedConfig;"
|
|
].join("\n");
|
|
}
|
|
}) : { dst: "" };
|
|
twCtx.set({ dst: template.dst });
|
|
return template;
|
|
};
|
|
const registerHooks = () => {
|
|
if (twCtx.use().meta?.disableHMR)
|
|
return;
|
|
const reloadConfigTemplate = async () => {
|
|
const { dst } = twCtx.use();
|
|
await loadConfigs();
|
|
setTimeout(async () => {
|
|
await updateTemplates({ filter: (t) => t.dst === dst || dst?.endsWith(t.filename) || false });
|
|
await nuxt.callHook("tailwindcss:internal:regenerateTemplates", { configTemplateUpdated: true });
|
|
}, 100);
|
|
};
|
|
nuxt.hook("app:templatesGenerated", async (_app, templates) => {
|
|
if (Array.isArray(templates) && templates?.some((t) => Object.keys(configUpdatedHook).includes(t.dst))) {
|
|
await reloadConfigTemplate();
|
|
}
|
|
});
|
|
if (moduleOptions.experimental?.strictScanContentPaths) {
|
|
nuxt.hook("pages:extend", async (pages) => {
|
|
const newPageFiles = resolvePageFiles(pages);
|
|
if (newPageFiles.length !== pagesContentPath.tryUse()?.length) {
|
|
pagesContentPath.set(newPageFiles, true);
|
|
await reloadConfigTemplate();
|
|
}
|
|
});
|
|
nuxt.hook("components:extend", async (components) => {
|
|
const newComponentFiles = components.map((c) => c.filePath);
|
|
if (newComponentFiles.length !== componentsContentPath.tryUse()?.length) {
|
|
componentsContentPath.set(newComponentFiles, true);
|
|
await reloadConfigTemplate();
|
|
}
|
|
});
|
|
} else {
|
|
nuxt.hook("pages:extend", () => reloadConfigTemplate());
|
|
}
|
|
nuxt.hook("vite:serverCreated", (server) => {
|
|
nuxt.hook("tailwindcss:internal:regenerateTemplates", (data) => {
|
|
if (!data || !data.configTemplateUpdated)
|
|
return;
|
|
const ctx = twCtx.use();
|
|
const configFile = ctx.dst && server.moduleGraph.getModuleById(ctx.dst);
|
|
configFile && server.moduleGraph.invalidateModule(configFile);
|
|
});
|
|
});
|
|
moduleOptions.exposeConfig && nuxt.hook("builder:watch", async (_, path) => {
|
|
if (Object.keys(configUpdatedHook).includes(join(nuxt.options.rootDir, path))) {
|
|
const ctx = twCtx.use();
|
|
setTimeout(async () => {
|
|
await import(ctx.dst).then(async (_config) => {
|
|
twCtx.set({ config: resolveTWConfig(_config.default ?? _config) });
|
|
await nuxt.callHook("tailwindcss:internal:regenerateTemplates");
|
|
});
|
|
}, 100);
|
|
}
|
|
});
|
|
};
|
|
return {
|
|
loadConfigs,
|
|
generateConfig,
|
|
registerHooks
|
|
};
|
|
};
|
|
|
|
const defaults = (nuxt = useNuxt()) => ({
|
|
configPath: [],
|
|
cssPath: join(nuxt.options.dir.assets, "css/tailwind.css"),
|
|
config: {},
|
|
viewer: nuxt.options.dev,
|
|
exposeConfig: false,
|
|
quiet: nuxt.options.logLevel === "silent",
|
|
editorSupport: false
|
|
});
|
|
const module = defineNuxtModule({
|
|
meta: { name, version, configKey, compatibility },
|
|
defaults,
|
|
async setup(moduleOptions, nuxt) {
|
|
if (moduleOptions.quiet)
|
|
logger.level = LogLevels.silent;
|
|
if (Number.parseFloat(getNuxtVersion()) < 2.16) {
|
|
await installModule("@nuxt/postcss8").catch((e) => {
|
|
logger.error(`Error occurred while loading \`@nuxt/postcss8\` required for Nuxt ${getNuxtVersion()}, is it installed?`);
|
|
throw e;
|
|
});
|
|
}
|
|
const isTailwind4 = await readPackageJSON("tailwindcss", { parent: import.meta.url }).then((m) => Number.parseFloat(m.version) >= 4);
|
|
if (isTailwind4 && !moduleOptions.experimental?.tailwindcss4) {
|
|
logger.warn("Tailwind CSS v4 detected. The current version of `@nuxtjs/tailwindcss` supports Tailwind CSS 3 officially and support for v4 is experimental. To suppress this warning, set `tailwindcss.experimental.tailwindcss4` to `true` in your `nuxt.config`.");
|
|
}
|
|
const ctx = await createInternalContext(moduleOptions, nuxt);
|
|
if (moduleOptions.editorSupport) {
|
|
const editorSupportConfig = resolveEditorSupportConfig(moduleOptions.editorSupport);
|
|
if (editorSupportConfig.autocompleteUtil && !isNuxtMajorVersion(2, nuxt)) {
|
|
addImports({
|
|
name: "autocompleteUtil",
|
|
from: createResolver(import.meta.url).resolve("./runtime/utils"),
|
|
as: "tw",
|
|
...typeof editorSupportConfig.autocompleteUtil === "object" ? editorSupportConfig.autocompleteUtil : {}
|
|
});
|
|
}
|
|
}
|
|
const [cssPath, cssPathConfig] = Array.isArray(moduleOptions.cssPath) ? moduleOptions.cssPath : [moduleOptions.cssPath];
|
|
const [resolvedCss, loggerInfo] = await resolveCSSPath(cssPath, nuxt).catch((e) => {
|
|
if (isTailwind4) {
|
|
return [addTemplate({ filename: "tailwind.css", getContents: () => `@import 'tailwindcss';`, write: true }).dst, "Generating default CSS file for Tailwind CSS 4..."];
|
|
}
|
|
throw e;
|
|
});
|
|
logger.info(loggerInfo);
|
|
nuxt.options.css = nuxt.options.css ?? [];
|
|
const resolvedNuxtCss = resolvedCss && await Promise.all(nuxt.options.css.map((p) => resolvePath(p.src ?? p))) || [];
|
|
if (resolvedCss && !resolvedNuxtCss.includes(resolvedCss)) {
|
|
const injectPosition = await resolveInjectPosition(resolvedNuxtCss, cssPathConfig?.injectPosition);
|
|
nuxt.options.css.splice(injectPosition, 0, resolvedCss);
|
|
}
|
|
const shouldInstallTWVitePlugin = isTailwind4 && nuxt.options.builder === "@nuxt/vite-builder";
|
|
if (shouldInstallTWVitePlugin) {
|
|
await import('@tailwindcss/vite').then((r) => addVitePlugin(r.default()));
|
|
}
|
|
let nuxt2ViewerConfig = join(nuxt.options.buildDir, "tailwind/postcss.mjs");
|
|
nuxt.hook("modules:done", async () => {
|
|
const _config = await ctx.loadConfigs();
|
|
const twConfig = ctx.generateConfig();
|
|
ctx.registerHooks();
|
|
nuxt2ViewerConfig = twConfig.dst || _config;
|
|
if (moduleOptions.exposeConfig) {
|
|
const exposeConfig = resolveExposeConfig(moduleOptions.exposeConfig);
|
|
const exposeTemplates = createExposeTemplates(exposeConfig);
|
|
nuxt.hook("tailwindcss:internal:regenerateTemplates", () => updateTemplates({ filter: (template) => exposeTemplates.includes(template.dst) }));
|
|
}
|
|
if (!shouldInstallTWVitePlugin) {
|
|
const postcssOptions = nuxt.options.postcss || nuxt.options.build.postcss.postcssOptions || nuxt.options.build.postcss;
|
|
const pluginsToAdd = isTailwind4 ? { "@tailwindcss/postcss": {} } : {
|
|
"tailwindcss/nesting": postcssOptions.plugins?.["tailwindcss/nesting"] ?? {},
|
|
"tailwindcss": twConfig.dst || _config
|
|
};
|
|
postcssOptions.plugins = {
|
|
...postcssOptions.plugins || {},
|
|
...pluginsToAdd
|
|
};
|
|
}
|
|
if (nuxt.options.dev && !isNuxtMajorVersion(2, nuxt)) {
|
|
if (moduleOptions.viewer) {
|
|
const viewerConfig = resolveViewerConfig(moduleOptions.viewer);
|
|
setupViewer(twConfig.dst || _config, viewerConfig, nuxt);
|
|
}
|
|
} else {
|
|
if (!nuxt.options.dev)
|
|
return;
|
|
if (moduleOptions.viewer) {
|
|
const viewerConfig = resolveViewerConfig(moduleOptions.viewer);
|
|
exportViewer(twConfig.dst || addTemplate({ filename: "tailwind.config/viewer-config.cjs", getContents: () => `module.exports = ${JSON.stringify(_config)}`, write: true }).dst, viewerConfig);
|
|
}
|
|
}
|
|
});
|
|
if (nuxt.options.dev && moduleOptions.viewer && isNuxtMajorVersion(2, nuxt)) {
|
|
const viewerConfig = resolveViewerConfig(moduleOptions.viewer);
|
|
setupViewer(nuxt2ViewerConfig, viewerConfig, nuxt);
|
|
}
|
|
}
|
|
});
|
|
|
|
export { module as default };
|