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"],
|
"aliases": ["c"],
|
||||||
"x-type": "component",
|
"x-type": "component",
|
||||||
"description": "Create a Vue 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": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
"peerDependencies": {},
|
"peerDependencies": {}
|
||||||
"exports": {
|
|
||||||
".": "./index.js",
|
|
||||||
"./package.json": "./package.json",
|
|
||||||
"./migrations.json": "./migrations.json",
|
|
||||||
"./generators.json": "./generators.json",
|
|
||||||
"./executors.json": "./executors.json"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,11 @@
|
|||||||
"outputPath": "build/packages/vue",
|
"outputPath": "build/packages/vue",
|
||||||
"tsConfig": "packages/vue/tsconfig.lib.json",
|
"tsConfig": "packages/vue/tsconfig.lib.json",
|
||||||
"main": "packages/vue/index.ts",
|
"main": "packages/vue/index.ts",
|
||||||
|
"generateExportsField": true,
|
||||||
|
"additionalEntryPoints": [
|
||||||
|
"{projectRoot}/{executors,generators,migrations}.json",
|
||||||
|
"{projectRoot}/src/tailwind.ts"
|
||||||
|
],
|
||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"input": "packages/vue",
|
"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 vueEslintConfigTypescriptVersion = '^11.0.3';
|
||||||
export const eslintPluginVueVersion = '^9.16.1';
|
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
|
// other deps
|
||||||
export const sassVersion = '1.62.1';
|
export const sassVersion = '1.62.1';
|
||||||
export const lessVersion = '3.12.2';
|
export const lessVersion = '3.12.2';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user