fix(nextjs): add setup generator for Tailwind CSS fixes absolute pathing (#30192)
<!-- Please make sure you have read the submission guidelines before posting an PR --> <!-- https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr --> <!-- Please make sure that your commit message follows our format --> <!-- Example: `fix(nx): must begin with lowercase` --> <!-- If this is a particularly complex change or feature addition, you can request a dedicated Nx release for this pull request branch. Mention someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they will confirm if the PR warrants its own release for testing purposes, and generate it for you if appropriate. --> ## Current Behavior <!-- This is the behavior we have today --> When we generate a Next.js application with tailwindcss running with `--turbo` fails due to how the globs are defined inside `tailwind.config.js`. ## Expected Behavior <!-- This is the behavior we should expect with the changes in this PR --> Relative paths a singular globs are required with this change tailwind should work with HMR using `--turbo` ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #29946
This commit is contained in:
parent
321a63aac3
commit
6d40b6a6ca
@ -7,7 +7,7 @@ import {
|
|||||||
Tree,
|
Tree,
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
import { initGenerator as jsInitGenerator } from '@nx/js';
|
import { initGenerator as jsInitGenerator } from '@nx/js';
|
||||||
import { setupTailwindGenerator } from '@nx/react';
|
import { setupTailwindGenerator } from '../setup-tailwind/setup-tailwind';
|
||||||
import {
|
import {
|
||||||
testingLibraryDomVersion,
|
testingLibraryDomVersion,
|
||||||
testingLibraryReactVersion,
|
testingLibraryReactVersion,
|
||||||
|
|||||||
@ -0,0 +1,15 @@
|
|||||||
|
const { join } = require('path');
|
||||||
|
|
||||||
|
// Note: If you use library-specific PostCSS/Tailwind configuration then you should remove the `postcssConfig` build
|
||||||
|
// option from your application's configuration (i.e. project.json).
|
||||||
|
//
|
||||||
|
// See: https://nx.dev/guides/using-tailwind-css-in-react#step-4:-applying-configuration-to-libraries
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {
|
||||||
|
config: join(__dirname, 'tailwind.config.js'),
|
||||||
|
},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
// const { createGlobPatternsForDependencies } = require('@nx/next/tailwind');
|
||||||
|
|
||||||
|
// The above utility import will not work if you are using Next.js' --turbo.
|
||||||
|
// Instead you will have to manually add the dependent paths to be included.
|
||||||
|
// For example
|
||||||
|
// ../libs/buttons/**/*.{ts,tsx,js,jsx,html}', <--- Adding a shared lib
|
||||||
|
// !../libs/buttons/**/*.{stories,spec}.{ts,tsx,js,jsx,html}', <--- Skip adding spec/stories files from shared lib
|
||||||
|
|
||||||
|
// If you are **not** using `--turbo` you can uncomment both lines 1 & 19.
|
||||||
|
// A discussion of the issue can be found: https://github.com/nrwl/nx/issues/26510
|
||||||
|
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
'./{src,pages,components,app}/**/*.{ts,tsx,js,jsx,html}',
|
||||||
|
'!./{src,pages,components,app}/**/*.{stories,spec}.{ts,tsx,js,jsx,html}',
|
||||||
|
// ...createGlobPatternsForDependencies(__dirname)
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
import {
|
||||||
|
joinPathFragments,
|
||||||
|
logger,
|
||||||
|
ProjectConfiguration,
|
||||||
|
stripIndents,
|
||||||
|
Tree,
|
||||||
|
} from '@nx/devkit';
|
||||||
|
|
||||||
|
import { SetupTailwindOptions } from '../schema';
|
||||||
|
|
||||||
|
// base directories and file types to simplify locating the stylesheet
|
||||||
|
const baseDirs = ['src', 'pages', 'src/pages', 'src/app', 'app'];
|
||||||
|
const fileNames = ['styles', 'global'];
|
||||||
|
const extensions = ['.css', '.scss', '.less'];
|
||||||
|
|
||||||
|
const knownLocations = baseDirs.flatMap((dir) =>
|
||||||
|
fileNames.flatMap((name) => extensions.map((ext) => `${dir}/${name}${ext}`))
|
||||||
|
);
|
||||||
|
|
||||||
|
export function addTailwindStyleImports(
|
||||||
|
tree: Tree,
|
||||||
|
project: ProjectConfiguration,
|
||||||
|
_options: SetupTailwindOptions
|
||||||
|
) {
|
||||||
|
const candidates = knownLocations.map((currentPath) =>
|
||||||
|
joinPathFragments(project.root, currentPath)
|
||||||
|
);
|
||||||
|
const stylesPath = candidates.find((currentStylePath) =>
|
||||||
|
tree.exists(currentStylePath)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (stylesPath) {
|
||||||
|
const content = tree.read(stylesPath).toString();
|
||||||
|
tree.write(
|
||||||
|
stylesPath,
|
||||||
|
`@tailwind base;\n@tailwind components;\n@tailwind utilities;\n${content}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
stripIndents`
|
||||||
|
Could not find stylesheet to update. Add the following imports to your stylesheet (e.g. styles.css):
|
||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
See our guide for more details: https://nx.dev/guides/using-tailwind-css-in-react`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import type { ProjectConfiguration, Tree } from '@nx/devkit';
|
||||||
|
import { joinPathFragments, updateProjectConfiguration } from '@nx/devkit';
|
||||||
|
|
||||||
|
import { SetupTailwindOptions } from '../schema';
|
||||||
|
|
||||||
|
export function updateProject(
|
||||||
|
tree: Tree,
|
||||||
|
config: ProjectConfiguration,
|
||||||
|
options: SetupTailwindOptions
|
||||||
|
) {
|
||||||
|
if (config?.targets?.build?.executor === '@nx/webpack:webpack') {
|
||||||
|
config.targets.build.options ??= {};
|
||||||
|
config.targets.build.options.postcssConfig = joinPathFragments(
|
||||||
|
config.root,
|
||||||
|
'postcss.config.js'
|
||||||
|
);
|
||||||
|
updateProjectConfiguration(tree, options.project, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
47
packages/next/src/generators/setup-tailwind/schema.json
Normal file
47
packages/next/src/generators/setup-tailwind/schema.json
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/schema",
|
||||||
|
"cli": "nx",
|
||||||
|
"$id": "NxNextTailwindSetupGenerator",
|
||||||
|
"title": "Configures Tailwind CSS for an application or a buildable/publishable library.",
|
||||||
|
"description": "Adds the Tailwind CSS configuration files for a Next.js 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"
|
||||||
|
},
|
||||||
|
"buildTarget": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the target used to build the project. This option is not needed in most cases.",
|
||||||
|
"default": "build",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["project"]
|
||||||
|
}
|
||||||
6
packages/next/src/generators/setup-tailwind/schema.ts
Normal file
6
packages/next/src/generators/setup-tailwind/schema.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export interface SetupTailwindOptions {
|
||||||
|
project: string;
|
||||||
|
buildTarget?: string;
|
||||||
|
skipFormat?: boolean;
|
||||||
|
skipPackageJson?: boolean;
|
||||||
|
}
|
||||||
@ -0,0 +1,176 @@
|
|||||||
|
import {
|
||||||
|
addProjectConfiguration,
|
||||||
|
readJson,
|
||||||
|
readProjectConfiguration,
|
||||||
|
stripIndents,
|
||||||
|
writeJson,
|
||||||
|
} from '@nx/devkit';
|
||||||
|
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||||
|
import update from './setup-tailwind';
|
||||||
|
|
||||||
|
describe('setup-tailwind', () => {
|
||||||
|
it.each`
|
||||||
|
stylesPath
|
||||||
|
${`src/styles.css`}
|
||||||
|
${`src/styles.scss`}
|
||||||
|
${`src/styles.less`}
|
||||||
|
${`pages/styles.css`}
|
||||||
|
${`pages/styles.scss`}
|
||||||
|
${`pages/styles.less`}
|
||||||
|
`('should update 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 add postcss and tailwind config files', async () => {
|
||||||
|
const tree = createTreeWithEmptyWorkspace();
|
||||||
|
addProjectConfiguration(tree, 'example', {
|
||||||
|
root: 'example',
|
||||||
|
sourceRoot: 'example/src',
|
||||||
|
targets: {
|
||||||
|
build: {
|
||||||
|
executor: '@nx/webpack:webpack',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
tree.write(`example/src/styles.css`, ``);
|
||||||
|
writeJson(tree, 'package.json', {
|
||||||
|
dependencies: {
|
||||||
|
react: '999.9.9',
|
||||||
|
},
|
||||||
|
devDependencies: {
|
||||||
|
'@types/react': '999.9.9',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await update(tree, {
|
||||||
|
project: 'example',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(tree.exists(`example/postcss.config.js`)).toBeTruthy();
|
||||||
|
expect(tree.exists(`example/tailwind.config.js`)).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
readProjectConfiguration(tree, 'example').targets.build.options
|
||||||
|
.postcssConfig
|
||||||
|
).toEqual('example/postcss.config.js');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip update if postcss 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/postcss.config.js', '// existing');
|
||||||
|
|
||||||
|
await update(tree, { project: 'example' });
|
||||||
|
|
||||||
|
expect(tree.read('example/postcss.config.js').toString()).toEqual(
|
||||||
|
'// existing'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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: {
|
||||||
|
react: '999.9.9',
|
||||||
|
},
|
||||||
|
devDependencies: {
|
||||||
|
'@types/react': '999.9.9',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await update(tree, {
|
||||||
|
project: 'example',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(readJson(tree, 'package.json')).toEqual({
|
||||||
|
dependencies: {
|
||||||
|
react: '999.9.9',
|
||||||
|
},
|
||||||
|
devDependencies: {
|
||||||
|
'@types/react': '999.9.9',
|
||||||
|
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: {
|
||||||
|
react: '999.9.9',
|
||||||
|
},
|
||||||
|
devDependencies: {
|
||||||
|
'@types/react': '999.9.9',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await update(tree, {
|
||||||
|
project: 'example',
|
||||||
|
skipPackageJson: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(readJson(tree, 'package.json')).toEqual({
|
||||||
|
dependencies: {
|
||||||
|
react: '999.9.9',
|
||||||
|
},
|
||||||
|
devDependencies: {
|
||||||
|
'@types/react': '999.9.9',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
import type { GeneratorCallback, Tree } from '@nx/devkit';
|
||||||
|
import {
|
||||||
|
addDependenciesToPackageJson,
|
||||||
|
formatFiles,
|
||||||
|
generateFiles,
|
||||||
|
joinPathFragments,
|
||||||
|
logger,
|
||||||
|
readProjectConfiguration,
|
||||||
|
runTasksInSerial,
|
||||||
|
} from '@nx/devkit';
|
||||||
|
|
||||||
|
import {
|
||||||
|
autoprefixerVersion,
|
||||||
|
postcssVersion,
|
||||||
|
tailwindcssVersion,
|
||||||
|
} from '@nx/react/src/utils/versions';
|
||||||
|
import type { SetupTailwindOptions } from './schema';
|
||||||
|
import { addTailwindStyleImports } from './lib/add-tailwind-style-imports';
|
||||||
|
import { updateProject } from './lib/update-project';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
export async function setupTailwindGenerator(
|
||||||
|
tree: Tree,
|
||||||
|
options: SetupTailwindOptions
|
||||||
|
) {
|
||||||
|
const tasks: GeneratorCallback[] = [];
|
||||||
|
const project = readProjectConfiguration(tree, options.project);
|
||||||
|
|
||||||
|
if (
|
||||||
|
tree.exists(joinPathFragments(project.root, 'postcss.config.js')) ||
|
||||||
|
tree.exists(joinPathFragments(project.root, 'tailwind.config.js'))
|
||||||
|
) {
|
||||||
|
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.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateFiles(tree, join(__dirname, './files'), project.root, {
|
||||||
|
tmpl: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
addTailwindStyleImports(tree, project, options);
|
||||||
|
|
||||||
|
updateProject(tree, project, options);
|
||||||
|
|
||||||
|
if (!options.skipPackageJson) {
|
||||||
|
tasks.push(
|
||||||
|
addDependenciesToPackageJson(
|
||||||
|
tree,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
autoprefixer: autoprefixerVersion,
|
||||||
|
postcss: postcssVersion,
|
||||||
|
tailwindcss: tailwindcssVersion,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.skipFormat) {
|
||||||
|
await formatFiles(tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
return runTasksInSerial(...tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default setupTailwindGenerator;
|
||||||
@ -1,3 +1,39 @@
|
|||||||
// Re-exporting for convenience and backwards compatibility.
|
import { createGlobPatternsForDependencies as jsGenerateGlobs } from '@nx/js/src/utils/generate-globs';
|
||||||
import { createGlobPatternsForDependencies } from '@nx/react/tailwind';
|
import { relative } from 'path';
|
||||||
export { createGlobPatternsForDependencies };
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
fileGlobPatternToInclude: string = '/**/*.{tsx,ts,jsx,js,html}',
|
||||||
|
fileGlobPatternToExclude: string = '/**/*.{stories,spec}.{tsx,ts,jsx,js,html}'
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
return [
|
||||||
|
...jsGenerateGlobs(dirPath, fileGlobPatternToInclude).map((glob) =>
|
||||||
|
relative(dirPath, glob)
|
||||||
|
),
|
||||||
|
...jsGenerateGlobs(dirPath, fileGlobPatternToExclude).map(
|
||||||
|
(glob) => `!${relative(dirPath, glob)}`
|
||||||
|
),
|
||||||
|
];
|
||||||
|
} 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
|
||||||
|
* fundamently 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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user