Skip to content

Commit

Permalink
feat(vue): add setup-tailwind generator
Browse files Browse the repository at this point in the history
  • Loading branch information
jaysoo committed Sep 14, 2023
1 parent 94f71cd commit 85842a5
Show file tree
Hide file tree
Showing 13 changed files with 382 additions and 8 deletions.
49 changes: 49 additions & 0 deletions e2e/vue/src/vue-tailwind.test.ts
Original file line number Diff line number Diff line change
@@ -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`,
`
<template>
<h1 className='text-3xl font-bold'>
Hello TailwindCSS!
</h1>
</template>
`
);

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);
});
5 changes: 5 additions & 0 deletions packages/vue/generators.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
}
9 changes: 1 addition & 8 deletions packages/vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}
}
5 changes: 5 additions & 0 deletions packages/vue/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const { join } = require('path');

module.exports = {
plugins: {
tailwindcss: {
config: join(__dirname, 'tailwind.config.js'),
},
autoprefixer: {},
},
}
Original file line number Diff line number Diff line change
@@ -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: [],
};
Original file line number Diff line number Diff line change
@@ -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}`
);
}
6 changes: 6 additions & 0 deletions packages/vue/src/generators/setup-tailwind/schema.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface SetupTailwindOptions {
project: string;
skipFormat?: boolean;
skipPackageJson?: boolean;
stylesheet?: string;
}
45 changes: 45 additions & 0 deletions packages/vue/src/generators/setup-tailwind/schema.json
Original file line number Diff line number Diff line change
@@ -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"]
}
114 changes: 114 additions & 0 deletions packages/vue/src/generators/setup-tailwind/setup-tailwind.spec.ts
Original file line number Diff line number Diff line change
@@ -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',
},
});
});
});
57 changes: 57 additions & 0 deletions packages/vue/src/generators/setup-tailwind/setup-tailwind.ts
Original file line number Diff line number Diff line change
@@ -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<void | GeneratorCallback> {
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;

0 comments on commit 85842a5

Please sign in to comment.