diff --git a/e2e/vue/src/vue-tailwind.test.ts b/e2e/vue/src/vue-tailwind.test.ts
new file mode 100644
index 00000000000000..daabb40e46d384
--- /dev/null
+++ b/e2e/vue/src/vue-tailwind.test.ts
@@ -0,0 +1,49 @@
+import {
+ cleanupProject,
+ listFiles,
+ newProject,
+ readFile,
+ runCLI,
+ uniq,
+ updateFile,
+} from '@nx/e2e/utils';
+
+describe('vue tailwind support', () => {
+ beforeAll(() => {
+ newProject({ unsetProjectNameAndRootFormat: false });
+ });
+
+ afterAll(() => {
+ cleanupProject();
+ });
+
+ it('should setup tailwind and build correctly', async () => {
+ const app = uniq('app');
+
+ runCLI(`generate @nx/vue:app ${app} --style=css --no-interactive`);
+ runCLI(`generate @nx/vue:setup-tailwind --project=${app}`);
+
+ updateFile(
+ `${app}/src/App.vue`,
+ `
+
+
+ Hello TailwindCSS!
+
+
+ `
+ );
+
+ runCLI(`build ${app}`);
+
+ const fileArray = listFiles(`dist/${app}/assets`);
+ const stylesheet = fileArray.find((file) => file.endsWith('.css'));
+ const content = readFile(`dist/${app}/assets/${stylesheet}`);
+
+ // used, not purged
+ expect(content).toContain('text-3xl');
+ expect(content).toContain('font-bold');
+ // unused, purged
+ expect(content).not.toContain('text-xl');
+ }, 300_000);
+});
diff --git a/packages/vue/generators.json b/packages/vue/generators.json
index 1f247e517b147f..2b52cb5da9f5e8 100644
--- a/packages/vue/generators.json
+++ b/packages/vue/generators.json
@@ -28,6 +28,11 @@
"aliases": ["c"],
"x-type": "component",
"description": "Create a Vue component."
+ },
+ "setup-tailwind": {
+ "factory": "./src/generators/setup-tailwind/setup-tailwind",
+ "schema": "./src/generators/setup-tailwind/schema.json",
+ "description": "Set up Tailwind configuration for a project."
}
}
}
diff --git a/packages/vue/package.json b/packages/vue/package.json
index c1f8ecf1aaeab4..ecf257c8c5b698 100644
--- a/packages/vue/package.json
+++ b/packages/vue/package.json
@@ -40,12 +40,5 @@
"publishConfig": {
"access": "public"
},
- "peerDependencies": {},
- "exports": {
- ".": "./index.js",
- "./package.json": "./package.json",
- "./migrations.json": "./migrations.json",
- "./generators.json": "./generators.json",
- "./executors.json": "./executors.json"
- }
+ "peerDependencies": {}
}
diff --git a/packages/vue/project.json b/packages/vue/project.json
index 012ce949398499..9c5d0856a9fdff 100644
--- a/packages/vue/project.json
+++ b/packages/vue/project.json
@@ -18,6 +18,11 @@
"outputPath": "build/packages/vue",
"tsConfig": "packages/vue/tsconfig.lib.json",
"main": "packages/vue/index.ts",
+ "generateExportsField": true,
+ "additionalEntryPoints": [
+ "{projectRoot}/{executors,generators,migrations}.json",
+ "{projectRoot}/src/tailwind.ts"
+ ],
"assets": [
{
"input": "packages/vue",
diff --git a/packages/vue/src/generators/setup-tailwind/files/postcss.config.js.template b/packages/vue/src/generators/setup-tailwind/files/postcss.config.js.template
new file mode 100644
index 00000000000000..ce46f1ea75cbcc
--- /dev/null
+++ b/packages/vue/src/generators/setup-tailwind/files/postcss.config.js.template
@@ -0,0 +1,10 @@
+const { join } = require('path');
+
+module.exports = {
+ plugins: {
+ tailwindcss: {
+ config: join(__dirname, 'tailwind.config.js'),
+ },
+ autoprefixer: {},
+ },
+}
diff --git a/packages/vue/src/generators/setup-tailwind/files/tailwind.config.js.template b/packages/vue/src/generators/setup-tailwind/files/tailwind.config.js.template
new file mode 100644
index 00000000000000..df80cf5a06476c
--- /dev/null
+++ b/packages/vue/src/generators/setup-tailwind/files/tailwind.config.js.template
@@ -0,0 +1,18 @@
+const { createGlobPatternsForDependencies } = require('@nx/vue/tailwind');
+const { join } = require('path');
+
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: [
+ join(__dirname, 'index.html'),
+ join(
+ __dirname,
+ 'src/**/*!(*.stories|*.spec).{vue,ts,tsx,js,jsx}'
+ ),
+ ...createGlobPatternsForDependencies(__dirname),
+ ],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+};
diff --git a/packages/vue/src/generators/setup-tailwind/lib/add-tailwind-style-imports.ts b/packages/vue/src/generators/setup-tailwind/lib/add-tailwind-style-imports.ts
new file mode 100644
index 00000000000000..9bc7ee45be5fa6
--- /dev/null
+++ b/packages/vue/src/generators/setup-tailwind/lib/add-tailwind-style-imports.ts
@@ -0,0 +1,37 @@
+import { joinPathFragments, ProjectConfiguration, Tree } from '@nx/devkit';
+
+import { SetupTailwindOptions } from '../schema';
+
+const knownStylesheetLocations = [
+ // What we generate by default
+ 'src/styles.css',
+ 'src/styles.scss',
+ 'src/styles.less',
+
+ // Other common locations (e.g. what `npm create vue` does)
+ 'src/assets/styles.css',
+ 'src/assets/styles.scss',
+ 'src/assets/styles.less',
+];
+
+export function addTailwindStyleImports(
+ tree: Tree,
+ project: ProjectConfiguration,
+ _options: SetupTailwindOptions
+) {
+ const stylesPath = knownStylesheetLocations
+ .map((file) => joinPathFragments(project.root, file))
+ .find((file) => tree.exists(file));
+
+ if (!stylesPath) {
+ throw new Error(
+ `Could not find the stylesheet to update. Use --stylesheet to specify this path (relative to the workspace root).`
+ );
+ }
+
+ const content = tree.read(stylesPath).toString();
+ tree.write(
+ stylesPath,
+ `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n${content}`
+ );
+}
diff --git a/packages/vue/src/generators/setup-tailwind/schema.d.ts b/packages/vue/src/generators/setup-tailwind/schema.d.ts
new file mode 100644
index 00000000000000..4ba7f47fea32f6
--- /dev/null
+++ b/packages/vue/src/generators/setup-tailwind/schema.d.ts
@@ -0,0 +1,6 @@
+export interface SetupTailwindOptions {
+ project: string;
+ skipFormat?: boolean;
+ skipPackageJson?: boolean;
+ stylesheet?: string;
+}
diff --git a/packages/vue/src/generators/setup-tailwind/schema.json b/packages/vue/src/generators/setup-tailwind/schema.json
new file mode 100644
index 00000000000000..bc56696ac858c3
--- /dev/null
+++ b/packages/vue/src/generators/setup-tailwind/schema.json
@@ -0,0 +1,45 @@
+{
+ "$schema": "http://json-schema.org/schema",
+ "cli": "nx",
+ "$id": "NxVueTailwindSetupGenerator",
+ "title": "Configures Tailwind CSS for an application or a library.",
+ "description": "Adds the Tailwind CSS configuration files for a given Vue project and installs, if needed, the packages required for Tailwind CSS to work.",
+ "type": "object",
+ "examples": [
+ {
+ "command": "nx g setup-tailwind --project=my-app",
+ "description": "Initialize Tailwind configuration for the `my-app` project."
+ }
+ ],
+ "properties": {
+ "project": {
+ "type": "string",
+ "description": "The name of the project to add the Tailwind CSS setup for.",
+ "alias": "p",
+ "$default": {
+ "$source": "argv",
+ "index": 0
+ },
+ "x-dropdown": "projects",
+ "x-prompt": "What project would you like to add the Tailwind CSS setup?",
+ "x-priority": "important"
+ },
+ "skipFormat": {
+ "type": "boolean",
+ "description": "Skips formatting the workspace after the generator completes.",
+ "x-priority": "internal"
+ },
+ "skipPackageJson": {
+ "type": "boolean",
+ "default": false,
+ "description": "Do not add dependencies to `package.json`.",
+ "x-priority": "internal"
+ },
+ "stylesheet": {
+ "type": "string",
+ "description": "Path to the styles entry point relative to the workspace root. This option is only needed if the stylesheet location cannot be found automatically."
+ }
+ },
+ "additionalProperties": false,
+ "required": ["project"]
+}
diff --git a/packages/vue/src/generators/setup-tailwind/setup-tailwind.spec.ts b/packages/vue/src/generators/setup-tailwind/setup-tailwind.spec.ts
new file mode 100644
index 00000000000000..ccf9790867b133
--- /dev/null
+++ b/packages/vue/src/generators/setup-tailwind/setup-tailwind.spec.ts
@@ -0,0 +1,114 @@
+import {
+ addProjectConfiguration,
+ readJson,
+ stripIndents,
+ writeJson,
+} from '@nx/devkit';
+import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
+import update from './setup-tailwind';
+
+describe('vue setup-tailwind generator', () => {
+ it.each`
+ stylesPath
+ ${`src/styles.css`}
+ ${`src/styles.scss`}
+ ${`src/styles.less`}
+ ${`src/assets/styles.css`}
+ ${`src/assets/styles.scss`}
+ ${`src/assets/styles.less`}
+ `('should update existing stylesheet', async ({ stylesPath }) => {
+ const tree = createTreeWithEmptyWorkspace();
+ addProjectConfiguration(tree, 'example', {
+ root: 'example',
+ sourceRoot: 'example/src',
+ targets: {},
+ });
+ tree.write(`example/${stylesPath}`, `/* existing content */`);
+
+ await update(tree, {
+ project: 'example',
+ });
+
+ expect(tree.read(`example/${stylesPath}`).toString()).toContain(
+ stripIndents`
+ @tailwind base;
+ @tailwind components;
+ @tailwind utilities;
+ /* existing content */
+ `
+ );
+ });
+
+ it('should skip update if tailwind configuration already exists', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+ addProjectConfiguration(tree, 'example', {
+ root: 'example',
+ sourceRoot: 'example/src',
+ targets: {},
+ });
+ tree.write(`example/src/styles.css`, ``);
+ tree.write('example/tailwind.config.js', '// existing');
+
+ await update(tree, { project: 'example' });
+
+ expect(tree.read('example/tailwind.config.js').toString()).toEqual(
+ '// existing'
+ );
+ });
+
+ it('should install packages', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+ addProjectConfiguration(tree, 'example', {
+ root: 'example',
+ sourceRoot: 'example/src',
+ targets: {},
+ });
+ tree.write(`example/src/styles.css`, ``);
+ writeJson(tree, 'package.json', {
+ dependencies: {
+ vue: '999.9.9',
+ },
+ });
+
+ await update(tree, {
+ project: 'example',
+ });
+
+ expect(readJson(tree, 'package.json')).toEqual({
+ dependencies: {
+ vue: '999.9.9',
+ },
+ devDependencies: {
+ autoprefixer: expect.any(String),
+ postcss: expect.any(String),
+ tailwindcss: expect.any(String),
+ },
+ });
+ });
+
+ it('should support skipping package install', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+ addProjectConfiguration(tree, 'example', {
+ root: 'example',
+ sourceRoot: 'example/src',
+ targets: {},
+ });
+ tree.write(`example/src/styles.css`, ``);
+ writeJson(tree, 'package.json', {
+ dependencies: {
+ vue: '999.9.9',
+ },
+ });
+
+ await update(tree, {
+ project: 'example',
+ skipPackageJson: true,
+ });
+
+ expect(readJson(tree, 'package.json')).toEqual({
+ dependencies: {
+ vue: '999.9.9',
+ },
+ });
+ });
+});
diff --git a/packages/vue/src/generators/setup-tailwind/setup-tailwind.ts b/packages/vue/src/generators/setup-tailwind/setup-tailwind.ts
new file mode 100644
index 00000000000000..ad5d54252ff682
--- /dev/null
+++ b/packages/vue/src/generators/setup-tailwind/setup-tailwind.ts
@@ -0,0 +1,57 @@
+import type { GeneratorCallback, Tree } from '@nx/devkit';
+import {
+ addDependenciesToPackageJson,
+ formatFiles,
+ generateFiles,
+ joinPathFragments,
+ logger,
+ readProjectConfiguration,
+} from '@nx/devkit';
+
+import {
+ autoprefixerVersion,
+ postcssVersion,
+ tailwindcssVersion,
+} from '../../utils/versions';
+import type { SetupTailwindOptions } from './schema';
+import { addTailwindStyleImports } from './lib/add-tailwind-style-imports';
+import { join } from 'path';
+
+export async function setupTailwindGenerator(
+ tree: Tree,
+ options: SetupTailwindOptions
+): Promise {
+ let installTask: GeneratorCallback | undefined = undefined;
+ const project = readProjectConfiguration(tree, options.project);
+
+ if (
+ tree.exists(joinPathFragments(project.root, 'postcss.config.js')) ||
+ tree.exists(joinPathFragments(project.root, 'tailwind.config.js'))
+ ) {
+ return logger.info(
+ `Skipping setup since there are existing PostCSS or Tailwind configuration files. For manual setup instructions, see https://nx.dev/guides/using-tailwind-css-in-react.`
+ );
+ }
+
+ generateFiles(tree, join(__dirname, './files'), project.root, {});
+
+ addTailwindStyleImports(tree, project, options);
+
+ if (!options.skipPackageJson) {
+ installTask = addDependenciesToPackageJson(
+ tree,
+ {},
+ {
+ autoprefixer: autoprefixerVersion,
+ postcss: postcssVersion,
+ tailwindcss: tailwindcssVersion,
+ }
+ );
+ }
+
+ if (!options.skipFormat) await formatFiles(tree);
+
+ return installTask;
+}
+
+export default setupTailwindGenerator;
diff --git a/packages/vue/src/tailwind.ts b/packages/vue/src/tailwind.ts
new file mode 100644
index 00000000000000..ef0dd75964c9e8
--- /dev/null
+++ b/packages/vue/src/tailwind.ts
@@ -0,0 +1,30 @@
+import { createGlobPatternsForDependencies as jsGenerateGlobs } from '@nx/js/src/utils/generate-globs';
+
+/**
+ * Generates a set of glob patterns based off the source root of the app and its dependencies
+ * @param dirPath workspace relative directory path that will be used to infer the parent project and dependencies
+ * @param fileGlobPattern pass a custom glob pattern to be used
+ */
+export function createGlobPatternsForDependencies(
+ dirPath: string,
+ fileGlobPattern: string = '/**/*!(*.stories|*.spec).{vue,tsx,ts,jsx,js}'
+) {
+ try {
+ return jsGenerateGlobs(dirPath, fileGlobPattern);
+ } catch (e) {
+ /**
+ * It should not be possible to reach this point when the utility is invoked as part of the normal
+ * lifecycle of Nx executors. However, other tooling, such as the VSCode Tailwind IntelliSense plugin
+ * or JetBrains editors such as WebStorm, may execute the tailwind.config.js file in order to provide
+ * autocomplete features, for example.
+ *
+ * In order to best support that use-case, we therefore do not hard error when the ProjectGraph is
+ * fundamentally unavailable in this tailwind-specific context.
+ */
+ console.warn(
+ '\nWARNING: There was an error creating glob patterns, returning an empty array\n' +
+ `${e.message}\n`
+ );
+ return [];
+ }
+}
diff --git a/packages/vue/src/utils/versions.ts b/packages/vue/src/utils/versions.ts
index 8d01f547ac118a..9a8632fca91a7d 100644
--- a/packages/vue/src/utils/versions.ts
+++ b/packages/vue/src/utils/versions.ts
@@ -18,6 +18,11 @@ export const vueEslintConfigPrettierVersion = '^8.0.0';
export const vueEslintConfigTypescriptVersion = '^11.0.3';
export const eslintPluginVueVersion = '^9.16.1';
+// tailwindcss
+export const postcssVersion = '8.4.21';
+export const tailwindcssVersion = '3.2.7';
+export const autoprefixerVersion = '10.4.13';
+
// other deps
export const sassVersion = '1.62.1';
export const lessVersion = '3.12.2';