feat(vue): add setup-tailwind generator (#19147)
This commit is contained in:
parent
d87826eea2
commit
e98221e142
49
e2e/vue/src/vue-tailwind.test.ts
Normal file
49
e2e/vue/src/vue-tailwind.test.ts
Normal file
@ -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);
|
||||
});
|
||||
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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": {}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
const { join } = require('path');
|
||||
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {
|
||||
config: join(__dirname, 'tailwind.config.js'),
|
||||
},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@ -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: [],
|
||||
};
|
||||
@ -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
packages/vue/src/generators/setup-tailwind/schema.d.ts
vendored
Normal file
6
packages/vue/src/generators/setup-tailwind/schema.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
export interface SetupTailwindOptions {
|
||||
project: string;
|
||||
skipFormat?: boolean;
|
||||
skipPackageJson?: boolean;
|
||||
stylesheet?: string;
|
||||
}
|
||||
45
packages/vue/src/generators/setup-tailwind/schema.json
Normal file
45
packages/vue/src/generators/setup-tailwind/schema.json
Normal file
@ -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"]
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
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 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',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
46
packages/vue/src/generators/setup-tailwind/setup-tailwind.ts
Normal file
46
packages/vue/src/generators/setup-tailwind/setup-tailwind.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import type { GeneratorCallback, Tree } from '@nx/devkit';
|
||||
import {
|
||||
addDependenciesToPackageJson,
|
||||
formatFiles,
|
||||
generateFiles,
|
||||
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);
|
||||
|
||||
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;
|
||||
30
packages/vue/src/tailwind.ts
Normal file
30
packages/vue/src/tailwind.ts
Normal file
@ -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 [];
|
||||
}
|
||||
}
|
||||
@ -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';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user