Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: nuxt module tailwind support #1033

Merged
merged 14 commits into from
Nov 4, 2024
6 changes: 6 additions & 0 deletions .changeset/little-boxes-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"uploadthing": patch
"@uploadthing/nuxt": patch
---

fix: dynamically add either stylesheet or tailwind plugin based on whether `@nuxtjs/tailwindcss´ is installed
1 change: 1 addition & 0 deletions packages/nuxt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@nuxt/module-builder": "^0.5.5",
"@nuxt/schema": "^3.11.2",
"@nuxt/test-utils": "^3.12.0",
"@nuxtjs/tailwindcss": "^6.12.2",
juliusmarminge marked this conversation as resolved.
Show resolved Hide resolved
"@uploadthing/eslint-config": "workspace:*",
"eslint": "^8.57.0",
"h3": "^1.11.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/nuxt/playground/app.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div>Playground</div>
<div class="text-lg font-bold">Playground</div>
<UploadButton
:config="{
endpoint: 'videoAndImage',
Expand Down
2 changes: 1 addition & 1 deletion packages/nuxt/playground/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: true },
modules: ["../src/module"],
modules: ["../src/module", "@nuxtjs/tailwindcss"],
telemetry: false,
});
1 change: 1 addition & 0 deletions packages/nuxt/playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"generate": "nuxi generate"
},
"devDependencies": {
"@nuxtjs/tailwindcss": "^6.12.2",
"nuxt": "^3.11.2"
}
}
71 changes: 68 additions & 3 deletions packages/nuxt/src/module.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
import { existsSync } from "node:fs";
import { resolve } from "node:path";
import { dirname, resolve, sep } from "node:path";
import {
addComponent,
addImports,
addServerHandler,
addTemplate,
createResolver,
defineNuxtModule,
hasNuxtModule,
resolvePath,
useLogger,
} from "@nuxt/kit";
import type { Resolver } from "@nuxt/kit";
import type { Nuxt } from "@nuxt/schema";
import defu from "defu";

import type { RouteHandlerConfig } from "uploadthing/internal/types";

// Module options TypeScript interface definition
export type ModuleOptions = RouteHandlerConfig & {
routerPath: string;
/**
* Injects UploadThing styles into the page
* If you're using Tailwind, it will inject the
* UploadThing Tailwind plugin instead.
*
* @default true
*/
injectStyles: boolean;
};

export default defineNuxtModule<ModuleOptions>({
Expand All @@ -28,6 +40,7 @@ export default defineNuxtModule<ModuleOptions>({
},
defaults: {
routerPath: "~/server/uploadthing",
injectStyles: true,
},
async setup(options, nuxt) {
const logger = useLogger("uploadthing");
Expand Down Expand Up @@ -70,8 +83,10 @@ export default defineNuxtModule<ModuleOptions>({
name: "UploadDropzone",
filePath: resolver.resolve("./runtime/components/dropzone"),
});
// FIXME: Use Tailwind Wrapper if the user has Tailwind installed
nuxt.options.css.push("@uploadthing/vue/styles.css");

if (options.injectStyles === true) {
await injectStyles(options, nuxt, resolver);
}

addImports({
name: "useUploadThing",
Expand All @@ -85,3 +100,53 @@ export default defineNuxtModule<ModuleOptions>({
});
},
});

async function injectStyles(
moduleOptions: ModuleOptions,
nuxt: Nuxt,
resolver: Resolver,
) {
/**
* Inject UploadThing stylesheet if no Tailwind is installed
*/
if (!hasNuxtModule("@nuxtjs/tailwindcss", nuxt)) {
nuxt.options.css.push("@uploadthing/vue/styles.css");
return;
}

/**
* Else we install our tailwind plugin
*/

const vueDist = await resolver.resolvePath("@uploadthing/vue");
const contentPath = dirname(vueDist) + sep + "**";
Comment on lines +126 to +127
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use path.join for more robust path handling

The current path construction using string concatenation with sep could be improved.

-const contentPath = dirname(vueDist) + sep + "**";
+const contentPath = path.join(dirname(vueDist), "**");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const vueDist = await resolver.resolvePath("@uploadthing/vue");
const contentPath = dirname(vueDist) + sep + "**";
const vueDist = await resolver.resolvePath("@uploadthing/vue");
const contentPath = path.join(dirname(vueDist), "**");


const template = addTemplate({
filename: "uploadthing.tailwind.config.cjs",
write: true,
getContents: () => `
const { uploadthingPlugin } = require('uploadthing/tw');

module.exports = {
content: [
${JSON.stringify(contentPath)}
],
plugins: [
require('uploadthing/tw').uploadthingPlugin
]
}
`,
});
Comment on lines +129 to +144
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider extracting Tailwind config template

The inline template could be moved to a separate file for better maintainability.

Consider creating a separate template file (e.g., templates/tailwind.config.cjs.template) and using it like this:

+import { readFileSync } from 'node:fs';
+
 const template = addTemplate({
   filename: "uploadthing.tailwind.config.cjs",
   write: true,
-  getContents: () => `
-    const { uploadthingPlugin } = require('uploadthing/tw');
-
-    module.exports = {
-      content: [
-        ${JSON.stringify(contentPath)}
-      ],
-      plugins: [
-        require('uploadthing/tw').uploadthingPlugin
-      ]
-    }
-  `,
+  getContents: () => {
+    const templateContent = readFileSync(
+      resolver.resolve('./templates/tailwind.config.cjs.template'),
+      'utf-8'
+    );
+    return templateContent.replace('{{contentPath}}', JSON.stringify(contentPath));
+  },
 });

Committable suggestion was skipped due to low confidence.


// @ts-expect-error - Help pls
juliusmarminge marked this conversation as resolved.
Show resolved Hide resolved
const twModuleOptions = (nuxt.options.tailwindcss ??= {}) as {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix type assertion to remove @ts-expect-error

The type assertion could be improved to properly type the Nuxt options.

-// @ts-expect-error - Help pls
-const twModuleOptions = (nuxt.options.tailwindcss ??= {}) as {
+interface TailwindOptions {
+  configPath?: string | string[];
+}
+const twModuleOptions = (nuxt.options.tailwindcss ??= {} as TailwindOptions);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// @ts-expect-error - Help pls
const twModuleOptions = (nuxt.options.tailwindcss ??= {}) as {
interface TailwindOptions {
configPath?: string | string[];
}
const twModuleOptions = (nuxt.options.tailwindcss ??= {} as TailwindOptions);
🧰 Tools
🪛 Biome

[error] 142-142: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

configPath?: string | string[];
};
if (typeof twModuleOptions.configPath === "string") {
twModuleOptions.configPath = [twModuleOptions.configPath, template.dst];
} else if (Array.isArray(twModuleOptions.configPath)) {
twModuleOptions.configPath.push(template.dst);
} else {
twModuleOptions.configPath = template.dst;
}
}
44 changes: 29 additions & 15 deletions packages/uploadthing/src/tw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,34 @@ import plugin from "tailwindcss/plugin";
*/
const PACKAGES = ["react", "solid", "svelte", "vue"];

/**
* UploadThing Tailwind plugin which injects custom variants
* for the built-in UI components
* @see https://docs.uploadthing.com/concepts/theming#theming-with-tailwind-css
*
* When using this, you need to specify `content` manually. For automatic
* detection, see {@link withUt}.
*/
export const uploadthingPlugin = plugin(({ addVariant }) => {
// Variants to select specific underlying element
addVariant("ut-button", '&>*[data-ut-element="button"]');
addVariant("ut-allowed-content", '&>*[data-ut-element="allowed-content"]');
addVariant("ut-label", '&>*[data-ut-element="label"]');
addVariant("ut-upload-icon", '&>*[data-ut-element="upload-icon"]');
addVariant("ut-clear-btn", '&>*[data-ut-element="clear-btn"]');

// Variants to select specific state
addVariant("ut-readying", '&[data-state="readying"]');
addVariant("ut-ready", '&[data-state="ready"]');
addVariant("ut-uploading", '&[data-state="uploading"]');
});

/**
* HOF for Tailwind config that adds the
* {@link uploadthingPlugin} to the Tailwind config
* as well as adds content paths to detect the necessary
* classnames
*/
export function withUt(twConfig: Config) {
const contentPaths = PACKAGES.map((pkg) => {
try {
Expand Down Expand Up @@ -41,25 +69,11 @@ export function withUt(twConfig: Config) {
twConfig.content.files.push(...contentPaths);
}

const utPlugin = plugin(({ addVariant }) => {
// Variants to select specific underlying element
addVariant("ut-button", '&>*[data-ut-element="button"]');
addVariant("ut-allowed-content", '&>*[data-ut-element="allowed-content"]');
addVariant("ut-label", '&>*[data-ut-element="label"]');
addVariant("ut-upload-icon", '&>*[data-ut-element="upload-icon"]');
addVariant("ut-clear-btn", '&>*[data-ut-element="clear-btn"]');

// Variants to select specific state
addVariant("ut-readying", '&[data-state="readying"]');
addVariant("ut-ready", '&[data-state="ready"]');
addVariant("ut-uploading", '&[data-state="uploading"]');
});

if (!twConfig.plugins) {
twConfig.plugins = [];
}

twConfig.plugins.push(utPlugin);
twConfig.plugins.push(uploadthingPlugin);

return twConfig;
}
Loading
Loading