feat(angular): add support for configuring tailwind in existing and new apps and buildable/publishable libs (#8043)

This commit is contained in:
Leosvel Pérez Espinosa 2021-12-16 17:25:32 +00:00 committed by GitHub
parent 45987c40d1
commit 2b00c99db1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 2199 additions and 580 deletions

View File

@ -33,6 +33,20 @@ nx g application ... --dry-run
## Options
### name (_**required**_)
Type: `string`
The name of the application.
### addTailwind
Default: `false`
Type: `boolean`
Whether to configure TailwindCSS for the application.
### backendProject
Type: `string`
@ -109,12 +123,6 @@ Possible values: `host`, `remote`
Type of application to generate the Module Federation configuration for.
### name
Type: `string`
The name of the application.
### port
Type: `number`

View File

@ -33,6 +33,12 @@ nx g library ... --dry-run
## Options
### name (_**required**_)
Type: `string`
The name of the library.
### addModuleSpec
Default: `false`
@ -41,6 +47,14 @@ Type: `boolean`
Add a module spec file.
### addTailwind
Default: `false`
Type: `boolean`
Whether to configure TailwindCSS for the application. It can only be used with buildable and publishable libraries. Non-buildable libraries will use the application's Tailwind configuration.
### buildable
Default: `false`
@ -87,12 +101,6 @@ Possible values: `eslint`, `none`
The tool to use for running lint checks.
### name
Type: `string`
The name of the library.
### parentModule
Type: `string`

View File

@ -0,0 +1,56 @@
---
title: '@nrwl/angular:setup-tailwind generator'
description: 'Configures TailwindCSS for an application or a buildable/publishable library.'
---
# @nrwl/angular:setup-tailwind
Configures TailwindCSS for an application or a buildable/publishable library.
## Usage
```bash
nx generate setup-tailwind ...
```
By default, Nx will search for `setup-tailwind` in the default collection provisioned in `angular.json`.
You can specify the collection explicitly as follows:
```bash
nx g @nrwl/angular:setup-tailwind ...
```
Show what will be generated without writing to disk:
```bash
nx g setup-tailwind ... --dry-run
```
## Options
### project (_**required**_)
Type: `string`
The name of the project to add the TailwindCSS setup for.
### buildTarget
Default: `build`
Type: `string`
The name of the target used to build the project. This option only applies to buildable/publishable libraries.
### skipFormat
Type: `boolean`
Skips formatting the workspace after the generator completes.
### stylesEntryPoint
Type: `string`
Path to the styles entry point relative to the workspace root. If not provided the generator will do its best to find it and it will error if it can't. This option only applies to applications.

View File

@ -498,6 +498,11 @@
"id": "setup-mfe",
"file": "angular/api-angular/generators/setup-mfe"
},
{
"name": "setup-tailwind generator",
"id": "setup-tailwind",
"file": "angular/api-angular/generators/setup-tailwind"
},
{
"name": "stories generator",
"id": "stories",
@ -1857,6 +1862,11 @@
"id": "setup-mfe",
"file": "react/api-angular/generators/setup-mfe"
},
{
"name": "setup-tailwind generator",
"id": "setup-tailwind",
"file": "react/api-angular/generators/setup-tailwind"
},
{
"name": "stories generator",
"id": "stories",
@ -3180,6 +3190,11 @@
"id": "setup-mfe",
"file": "node/api-angular/generators/setup-mfe"
},
{
"name": "setup-tailwind generator",
"id": "setup-tailwind",
"file": "node/api-angular/generators/setup-tailwind"
},
{
"name": "stories generator",
"id": "stories",

View File

@ -33,6 +33,20 @@ nx g application ... --dry-run
## Options
### name (_**required**_)
Type: `string`
The name of the application.
### addTailwind
Default: `false`
Type: `boolean`
Whether to configure TailwindCSS for the application.
### backendProject
Type: `string`
@ -109,12 +123,6 @@ Possible values: `host`, `remote`
Type of application to generate the Module Federation configuration for.
### name
Type: `string`
The name of the application.
### port
Type: `number`

View File

@ -33,6 +33,12 @@ nx g library ... --dry-run
## Options
### name (_**required**_)
Type: `string`
The name of the library.
### addModuleSpec
Default: `false`
@ -41,6 +47,14 @@ Type: `boolean`
Add a module spec file.
### addTailwind
Default: `false`
Type: `boolean`
Whether to configure TailwindCSS for the application. It can only be used with buildable and publishable libraries. Non-buildable libraries will use the application's Tailwind configuration.
### buildable
Default: `false`
@ -87,12 +101,6 @@ Possible values: `eslint`, `none`
The tool to use for running lint checks.
### name
Type: `string`
The name of the library.
### parentModule
Type: `string`

View File

@ -0,0 +1,56 @@
---
title: '@nrwl/angular:setup-tailwind generator'
description: 'Configures TailwindCSS for an application or a buildable/publishable library.'
---
# @nrwl/angular:setup-tailwind
Configures TailwindCSS for an application or a buildable/publishable library.
## Usage
```bash
nx generate setup-tailwind ...
```
By default, Nx will search for `setup-tailwind` in the default collection provisioned in `workspace.json`.
You can specify the collection explicitly as follows:
```bash
nx g @nrwl/angular:setup-tailwind ...
```
Show what will be generated without writing to disk:
```bash
nx g setup-tailwind ... --dry-run
```
## Options
### project (_**required**_)
Type: `string`
The name of the project to add the TailwindCSS setup for.
### buildTarget
Default: `build`
Type: `string`
The name of the target used to build the project. This option only applies to buildable/publishable libraries.
### skipFormat
Type: `boolean`
Skips formatting the workspace after the generator completes.
### stylesEntryPoint
Type: `string`
Path to the styles entry point relative to the workspace root. If not provided the generator will do its best to find it and it will error if it can't. This option only applies to applications.

View File

@ -33,6 +33,20 @@ nx g application ... --dry-run
## Options
### name (_**required**_)
Type: `string`
The name of the application.
### addTailwind
Default: `false`
Type: `boolean`
Whether to configure TailwindCSS for the application.
### backendProject
Type: `string`
@ -109,12 +123,6 @@ Possible values: `host`, `remote`
Type of application to generate the Module Federation configuration for.
### name
Type: `string`
The name of the application.
### port
Type: `number`

View File

@ -33,6 +33,12 @@ nx g library ... --dry-run
## Options
### name (_**required**_)
Type: `string`
The name of the library.
### addModuleSpec
Default: `false`
@ -41,6 +47,14 @@ Type: `boolean`
Add a module spec file.
### addTailwind
Default: `false`
Type: `boolean`
Whether to configure TailwindCSS for the application. It can only be used with buildable and publishable libraries. Non-buildable libraries will use the application's Tailwind configuration.
### buildable
Default: `false`
@ -87,12 +101,6 @@ Possible values: `eslint`, `none`
The tool to use for running lint checks.
### name
Type: `string`
The name of the library.
### parentModule
Type: `string`

View File

@ -0,0 +1,56 @@
---
title: '@nrwl/angular:setup-tailwind generator'
description: 'Configures TailwindCSS for an application or a buildable/publishable library.'
---
# @nrwl/angular:setup-tailwind
Configures TailwindCSS for an application or a buildable/publishable library.
## Usage
```bash
nx generate setup-tailwind ...
```
By default, Nx will search for `setup-tailwind` in the default collection provisioned in `workspace.json`.
You can specify the collection explicitly as follows:
```bash
nx g @nrwl/angular:setup-tailwind ...
```
Show what will be generated without writing to disk:
```bash
nx g setup-tailwind ... --dry-run
```
## Options
### project (_**required**_)
Type: `string`
The name of the project to add the TailwindCSS setup for.
### buildTarget
Default: `build`
Type: `string`
The name of the target used to build the project. This option only applies to buildable/publishable libraries.
### skipFormat
Type: `boolean`
Skips formatting the workspace after the generator completes.
### stylesEntryPoint
Type: `string`
Path to the styles entry point relative to the workspace root. If not provided the generator will do its best to find it and it will error if it can't. This option only applies to applications.

View File

@ -94,22 +94,10 @@ describe('Angular Package', () => {
runCLI('run-many --target build --all --parallel');
});
it('should support Ivy', async () => {
const myapp = uniq('myapp');
runCLI(
`generate @nrwl/angular:app ${myapp} --directory=myDir --routing --enable-ivy`
);
runCLI(`build my-dir-${myapp} --aot`);
expectTestsPass(await runCLIAsync(`test my-dir-${myapp} --no-watch`));
}, 1000000);
it('should support workspaces w/o workspace config file', async () => {
removeFile('workspace.json');
const myapp = uniq('myapp');
runCLI(
`generate @nrwl/angular:app ${myapp} --directory=myDir --routing --enable-ivy`
);
runCLI(`generate @nrwl/angular:app ${myapp} --directory=myDir --routing`);
runCLI(`build my-dir-${myapp} --aot`);
expectTestsPass(await runCLIAsync(`test my-dir-${myapp} --no-watch`));

View File

@ -4,8 +4,6 @@ import {
checkFilesExist,
getSelectedPackageManager,
newProject,
packageInstall,
readFile,
readJson,
cleanupProject,
runCLI,
@ -13,7 +11,6 @@ import {
updateFile,
} from '@nrwl/e2e/utils';
import { names } from '@nrwl/devkit';
import { join } from 'path';
describe('Angular Package', () => {
['publishable', 'buildable'].forEach((testConfig) => {
@ -197,253 +194,4 @@ describe('Angular Package', () => {
expect(buildOutput).toContain('Running target "build" succeeded');
});
});
describe('Tailwind support', () => {
let projectScope: string;
let buildLibExecutorOption: string;
let buildLibProjectConfig: string;
let buildLibRootConfig: string;
let pubLibExecutorOption: string;
let pubLibProjectConfig: string;
let pubLibRootConfig: string;
const customTailwindConfigFile = 'custom-tailwind.config.js';
const spacing = {
rootConfig: {
sm: '2px',
md: '4px',
lg: '8px',
},
projectConfig: {
sm: '1px',
md: '2px',
lg: '4px',
},
executorOption: {
sm: '4px',
md: '8px',
lg: '16px',
},
};
const createWorkspaceTailwindConfigFile = () => {
const tailwindConfigFile = 'tailwind.config.js';
const tailwindConfig = `module.exports = {
mode: 'jit',
purge: ['./apps/**/*.{html,ts}', './libs/**/*.{html,ts}'],
darkMode: false,
theme: {
spacing: {
sm: '${spacing.rootConfig.sm}',
md: '${spacing.rootConfig.md}',
lg: '${spacing.rootConfig.lg}',
},
},
variants: { extend: {} },
plugins: [],
};
`;
updateFile(tailwindConfigFile, tailwindConfig);
};
const createTailwindConfigFile = (
dir: string,
lib: string,
libSpacing: typeof spacing['executorOption'],
tailwindConfigFile = 'tailwind.config.js'
) => {
const tailwindConfigFilePath = join(dir, tailwindConfigFile);
const tailwindConfig = `const { createGlobPatternsForDependencies } = require('@nrwl/angular/tailwind');
module.exports = {
mode: 'jit',
purge: [
'./libs/${lib}/src/**/*.{html,ts}',
...createGlobPatternsForDependencies(__dirname),
],
darkMode: false,
theme: {
spacing: {
sm: '${libSpacing.sm}',
md: '${libSpacing.md}',
lg: '${libSpacing.lg}',
},
},
variants: { extend: {} },
plugins: [],
};
`;
updateFile(tailwindConfigFilePath, tailwindConfig);
};
const addTailwindConfigToProject = (lib: string) => {
const angularJson = readJson('angular.json');
angularJson.projects[
lib
].architect.build.options.tailwindConfig = `libs/${lib}/${customTailwindConfigFile}`;
updateFile('angular.json', JSON.stringify(angularJson, null, 2));
};
const createLibComponent = (lib: string) => {
updateFile(
`libs/${lib}/src/lib/foo.component.ts`,
`import { Component } from '@angular/core';
@Component({
selector: '${projectScope}-foo',
template: '<button class="custom-btn">Click me!</button>',
styles: [\`
.custom-btn {
@apply m-md p-sm;
}
\`]
})
export class FooComponent {}
`
);
updateFile(
`libs/${lib}/src/lib/${lib}.module.ts`,
`import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FooComponent } from './foo.component';
@NgModule({
imports: [CommonModule],
declarations: [FooComponent],
exports: [FooComponent],
})
export class LibModule {}
`
);
updateFile(
`libs/${lib}/src/index.ts`,
`export * from './lib/foo.component';
export * from './lib/${lib}.module';
`
);
};
beforeEach(() => {
const projectName = uniq('proj');
projectScope = newProject({ name: projectName });
buildLibExecutorOption = uniq('build-lib-executor-option');
buildLibProjectConfig = uniq('build-lib-project-config');
buildLibRootConfig = uniq('build-lib-root-config');
pubLibExecutorOption = uniq('pub-lib-executor-option');
pubLibProjectConfig = uniq('pub-lib-project-config');
pubLibRootConfig = uniq('pub-lib-root-config');
// Install Tailwind required packages.
// TODO: Remove this when Tailwind generator is created and used.
packageInstall('tailwindcss postcss autoprefixer', projectName, 'latest');
// Create Tailwind config in the workspace root.
createWorkspaceTailwindConfigFile();
// Setup buildable libs
// Buildable lib with tailwind config specified in the project config
runCLI(
`generate @nrwl/angular:lib ${buildLibExecutorOption} --buildable --no-interactive`
);
createLibComponent(buildLibExecutorOption);
createTailwindConfigFile(
`libs/${buildLibExecutorOption}`,
buildLibExecutorOption,
spacing.executorOption,
customTailwindConfigFile
);
addTailwindConfigToProject(buildLibExecutorOption);
// Buildable lib with tailwind config located in the project root
runCLI(
`generate @nrwl/angular:lib ${buildLibProjectConfig} --buildable --no-interactive`
);
createLibComponent(buildLibProjectConfig);
createTailwindConfigFile(
`libs/${buildLibProjectConfig}`,
buildLibProjectConfig,
spacing.projectConfig
);
// Buildable lib with tailwind config located in the workspace root
runCLI(
`generate @nrwl/angular:lib ${buildLibRootConfig} --buildable --no-interactive`
);
createLibComponent(buildLibRootConfig);
// Publishable libs
// Publishable lib with tailwind config specified in the project config
runCLI(
`generate @nrwl/angular:lib ${pubLibExecutorOption} --publishable --importPath=@${projectScope}/${pubLibExecutorOption} --no-interactive`
);
createLibComponent(pubLibExecutorOption);
createTailwindConfigFile(
`libs/${pubLibExecutorOption}`,
pubLibExecutorOption,
spacing.executorOption,
customTailwindConfigFile
);
addTailwindConfigToProject(pubLibExecutorOption);
// Publishable lib with tailwind config located in the project root
runCLI(
`generate @nrwl/angular:lib ${pubLibProjectConfig} --publishable --importPath=@${projectScope}/${pubLibProjectConfig} --no-interactive`
);
createLibComponent(pubLibProjectConfig);
createTailwindConfigFile(
`libs/${pubLibProjectConfig}`,
pubLibProjectConfig,
spacing.projectConfig
);
// Publishable lib with tailwind config located in the workspace root
runCLI(
`generate @nrwl/angular:lib ${pubLibRootConfig} --publishable --importPath=@${projectScope}/${pubLibRootConfig} --no-interactive`
);
createLibComponent(pubLibRootConfig);
});
const assertComponentStyles = (
lib: string,
libSpacing: typeof spacing['executorOption']
) => {
const builtComponentContent = readFile(
`dist/libs/${lib}/esm2020/lib/foo.component.mjs`
);
let expectedStylesRegex = new RegExp(
`styles: \\[\\"\\.custom\\-btn(\\[_ngcontent\\-%COMP%\\])?{margin:${libSpacing.md};padding:${libSpacing.sm}}(\\\\n)?\\"\\]`
);
expect(builtComponentContent).toMatch(expectedStylesRegex);
};
it('should build and output the right styles based on the tailwind config', () => {
runCLI(`build ${buildLibExecutorOption}`);
assertComponentStyles(buildLibExecutorOption, spacing.executorOption);
runCLI(`build ${buildLibProjectConfig}`);
assertComponentStyles(buildLibProjectConfig, spacing.projectConfig);
runCLI(`build ${buildLibRootConfig}`);
assertComponentStyles(buildLibRootConfig, spacing.rootConfig);
runCLI(`build ${pubLibExecutorOption}`);
assertComponentStyles(pubLibExecutorOption, spacing.executorOption);
runCLI(`build ${pubLibProjectConfig}`);
assertComponentStyles(pubLibProjectConfig, spacing.projectConfig);
runCLI(`build ${pubLibRootConfig}`);
assertComponentStyles(pubLibRootConfig, spacing.rootConfig);
});
});
});

View File

@ -0,0 +1,398 @@
process.env.SELECTED_CLI = 'angular';
import {
cleanupProject,
listFiles,
newProject,
readFile,
removeFile,
runCLI,
uniq,
updateFile,
} from '@nrwl/e2e/utils';
describe('Tailwind support', () => {
let project: string;
const defaultButtonBgColor = 'bg-blue-700';
const buildLibWithTailwind = {
name: uniq('build-lib-with-tailwind'),
buttonBgColor: 'bg-green-800',
};
const pubLibWithTailwind = {
name: uniq('pub-lib-with-tailwind'),
buttonBgColor: 'bg-red-900',
};
const spacing = {
root: {
sm: '2px',
md: '4px',
lg: '8px',
},
projectVariant1: {
sm: '1px',
md: '2px',
lg: '4px',
},
projectVariant2: {
sm: '4px',
md: '8px',
lg: '16px',
},
projectVariant3: {
sm: '8px',
md: '16px',
lg: '32px',
},
};
const createWorkspaceTailwindConfigFile = () => {
const tailwindConfigFile = 'tailwind.config.js';
const tailwindConfig = `module.exports = {
mode: 'jit',
purge: ['./apps/**/*.{html,ts}', './libs/**/*.{html,ts}'],
darkMode: false,
theme: {
spacing: {
sm: '${spacing.root.sm}',
md: '${spacing.root.md}',
lg: '${spacing.root.lg}',
},
},
variants: { extend: {} },
plugins: [],
};
`;
updateFile(tailwindConfigFile, tailwindConfig);
};
const createTailwindConfigFile = (
tailwindConfigFile = 'tailwind.config.js',
libSpacing: typeof spacing['projectVariant1']
) => {
const tailwindConfig = `const { createGlobPatternsForDependencies } = require('@nrwl/angular/tailwind');
const { join } = require('path');
module.exports = {
mode: 'jit',
purge: [
join(__dirname, 'src/**/*.{html,ts}'),
...createGlobPatternsForDependencies(__dirname),
],
darkMode: false,
theme: {
spacing: {
sm: '${libSpacing.sm}',
md: '${libSpacing.md}',
lg: '${libSpacing.lg}',
},
},
variants: { extend: {} },
plugins: [],
};
`;
updateFile(tailwindConfigFile, tailwindConfig);
};
const updateTailwindConfig = (
tailwindConfigPath: string,
projectSpacing: typeof spacing['root']
) => {
const tailwindConfig = readFile(tailwindConfigPath);
const tailwindConfigUpdated = tailwindConfig.replace(
'theme: {',
`theme: {
spacing: {
sm: '${projectSpacing.sm}',
md: '${projectSpacing.md}',
lg: '${projectSpacing.lg}',
},`
);
updateFile(tailwindConfigPath, tailwindConfigUpdated);
};
beforeAll(() => {
project = newProject();
// Create tailwind config in the workspace root
createWorkspaceTailwindConfigFile();
});
afterAll(() => cleanupProject());
describe('Libraries', () => {
const createLibComponent = (
lib: string,
buttonBgColor: string = defaultButtonBgColor
) => {
updateFile(
`libs/${lib}/src/lib/foo.component.ts`,
`import { Component } from '@angular/core';
@Component({
selector: '${project}-foo',
template: '<button class="custom-btn text-white ${buttonBgColor}">Click me!</button>',
styles: [\`
.custom-btn {
@apply m-md p-sm;
}
\`]
})
export class FooComponent {}
`
);
updateFile(
`libs/${lib}/src/lib/${lib}.module.ts`,
`import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FooComponent } from './foo.component';
@NgModule({
imports: [CommonModule],
declarations: [FooComponent],
exports: [FooComponent],
})
export class LibModule {}
`
);
updateFile(
`libs/${lib}/src/index.ts`,
`export * from './lib/foo.component';
export * from './lib/${lib}.module';
`
);
};
const assertLibComponentStyles = (
lib: string,
libSpacing: typeof spacing['root']
) => {
const builtComponentContent = readFile(
`dist/libs/${lib}/esm2020/lib/foo.component.mjs`
);
let expectedStylesRegex = new RegExp(
`styles: \\[\\"\\.custom\\-btn(\\[_ngcontent\\-%COMP%\\])?{margin:${libSpacing.md};padding:${libSpacing.sm}}(\\\\n)?\\"\\]`
);
expect(builtComponentContent).toMatch(expectedStylesRegex);
};
it('should generate a buildable library with tailwind and build correctly', () => {
runCLI(
`generate @nrwl/angular:lib ${buildLibWithTailwind.name} --buildable --add-tailwind --no-interactive`
);
updateTailwindConfig(
`libs/${buildLibWithTailwind.name}/tailwind.config.js`,
spacing.projectVariant1
);
createLibComponent(
buildLibWithTailwind.name,
buildLibWithTailwind.buttonBgColor
);
runCLI(`build ${buildLibWithTailwind.name}`);
assertLibComponentStyles(
buildLibWithTailwind.name,
spacing.projectVariant1
);
});
it('should set up tailwind in a previously generated buildable library and build correctly', () => {
const buildLibSetupTailwind = uniq('build-lib-setup-tailwind');
runCLI(
`generate @nrwl/angular:lib ${buildLibSetupTailwind} --buildable --no-interactive`
);
runCLI(
`generate @nrwl/angular:setup-tailwind ${buildLibSetupTailwind} --no-interactive`
);
updateTailwindConfig(
`libs/${buildLibSetupTailwind}/tailwind.config.js`,
spacing.projectVariant2
);
createLibComponent(buildLibSetupTailwind);
runCLI(`build ${buildLibSetupTailwind}`);
assertLibComponentStyles(buildLibSetupTailwind, spacing.projectVariant2);
});
it('should correctly build a buildable library with a tailwind.config.js file in the project root or workspace root', () => {
const buildLibNoProjectConfig = uniq('build-lib-no-project-config');
runCLI(
`generate @nrwl/angular:lib ${buildLibNoProjectConfig} --buildable --no-interactive`
);
createTailwindConfigFile(
`libs/${buildLibNoProjectConfig}/tailwind.config.js`,
spacing.projectVariant3
);
createLibComponent(buildLibNoProjectConfig);
runCLI(`build ${buildLibNoProjectConfig}`);
assertLibComponentStyles(
buildLibNoProjectConfig,
spacing.projectVariant3
);
// remove tailwind.config.js file from the project root to test the one in the workspace root
removeFile(`libs/${buildLibNoProjectConfig}/tailwind.config.js`);
runCLI(`build ${buildLibNoProjectConfig}`);
assertLibComponentStyles(buildLibNoProjectConfig, spacing.root);
});
it('should generate a publishable library with tailwind and build correctly', () => {
runCLI(
`generate @nrwl/angular:lib ${pubLibWithTailwind.name} --publishable --add-tailwind --importPath=@${project}/${pubLibWithTailwind.name} --no-interactive`
);
updateTailwindConfig(
`libs/${pubLibWithTailwind.name}/tailwind.config.js`,
spacing.projectVariant1
);
createLibComponent(
pubLibWithTailwind.name,
pubLibWithTailwind.buttonBgColor
);
runCLI(`build ${pubLibWithTailwind.name}`);
assertLibComponentStyles(
pubLibWithTailwind.name,
spacing.projectVariant1
);
});
it('should set up tailwind in a previously generated publishable library and build correctly', () => {
const pubLibSetupTailwind = uniq('pub-lib-setup-tailwind');
runCLI(
`generate @nrwl/angular:lib ${pubLibSetupTailwind} --publishable --importPath=@${project}/${pubLibSetupTailwind} --no-interactive`
);
runCLI(
`generate @nrwl/angular:setup-tailwind ${pubLibSetupTailwind} --no-interactive`
);
updateTailwindConfig(
`libs/${pubLibSetupTailwind}/tailwind.config.js`,
spacing.projectVariant2
);
createLibComponent(pubLibSetupTailwind);
runCLI(`build ${pubLibSetupTailwind}`);
assertLibComponentStyles(pubLibSetupTailwind, spacing.projectVariant2);
});
it('should correctly build a publishable library with a tailwind.config.js file in the project root or workspace root', () => {
const pubLibNoProjectConfig = uniq('pub-lib-no-project-config');
runCLI(
`generate @nrwl/angular:lib ${pubLibNoProjectConfig} --publishable --importPath=@${project}/${pubLibNoProjectConfig} --no-interactive`
);
createTailwindConfigFile(
`libs/${pubLibNoProjectConfig}/tailwind.config.js`,
spacing.projectVariant3
);
createLibComponent(pubLibNoProjectConfig);
runCLI(`build ${pubLibNoProjectConfig}`);
assertLibComponentStyles(pubLibNoProjectConfig, spacing.projectVariant3);
// remove tailwind.config.js file from the project root to test the one in the workspace root
removeFile(`libs/${pubLibNoProjectConfig}/tailwind.config.js`);
runCLI(`build ${pubLibNoProjectConfig}`);
assertLibComponentStyles(pubLibNoProjectConfig, spacing.root);
});
});
describe('Applications', () => {
const updateAppComponent = (app: string) => {
updateFile(
`apps/${app}/src/app/app.component.html`,
`<button class="custom-btn text-white">Click me!</button>`
);
updateFile(
`apps/${app}/src/app/app.component.css`,
`.custom-btn {
@apply m-md p-sm;
}`
);
};
const readAppStylesBundle = (app: string) => {
const stylesBundlePath = listFiles(`dist/apps/${app}`).find((file) =>
file.startsWith('styles.')
);
const stylesBundle = readFile(`dist/apps/${app}/${stylesBundlePath}`);
return stylesBundle;
};
const assertAppComponentStyles = (
app: string,
appSpacing: typeof spacing['root']
) => {
const mainBundlePath = listFiles(`dist/apps/${app}`).find((file) =>
file.startsWith('main.')
);
const mainBundle = readFile(`dist/apps/${app}/${mainBundlePath}`);
let expectedStylesRegex = new RegExp(
`styles:\\[\\"\\.custom\\-btn\\[_ngcontent\\-%COMP%\\]{margin:${appSpacing.md};padding:${appSpacing.sm}}\\"\\]`
);
expect(mainBundle).toMatch(expectedStylesRegex);
};
it('should build correctly and only output the tailwind utilities used', () => {
const appWithTailwind = uniq('app-with-tailwind');
runCLI(
`generate @nrwl/angular:app ${appWithTailwind} --add-tailwind --no-interactive`
);
updateTailwindConfig(
`apps/${appWithTailwind}/tailwind.config.js`,
spacing.projectVariant1
);
updateFile(
`apps/${appWithTailwind}/src/app/app.module.ts`,
`import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { LibModule as LibModule1 } from '@${project}/${buildLibWithTailwind.name}';
import { LibModule as LibModule2 } from '@${project}/${pubLibWithTailwind.name}';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, LibModule1, LibModule2],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
`
);
updateAppComponent(appWithTailwind);
runCLI(`build ${appWithTailwind}`);
assertAppComponentStyles(appWithTailwind, spacing.projectVariant1);
let stylesBundle = readAppStylesBundle(appWithTailwind);
expect(stylesBundle).toContain('.text-white');
expect(stylesBundle).not.toContain('.text-black');
expect(stylesBundle).toContain(`.${buildLibWithTailwind.buttonBgColor}`);
expect(stylesBundle).toContain(`.${pubLibWithTailwind.buttonBgColor}`);
expect(stylesBundle).not.toContain(`.${defaultButtonBgColor}`);
});
});
});

View File

@ -79,6 +79,31 @@
"schema": "./src/generators/ngrx/schema.json",
"description": "Adds NgRx support to an application or library."
},
"scam": {
"factory": "./src/generators/scam/scam.compat",
"schema": "./src/generators/scam/schema.json",
"description": "Generate a component with an accompanying Single Component Angular Module (SCAM)."
},
"scam-directive": {
"factory": "./src/generators/scam-directive/scam-directive.compat",
"schema": "./src/generators/scam-directive/schema.json",
"description": "Generate a directive with an accompanying Single Component Angular Module (SCAM)."
},
"scam-pipe": {
"factory": "./src/generators/scam-pipe/scam-pipe.compat",
"schema": "./src/generators/scam-pipe/schema.json",
"description": "Generate a pipe with an accompanying Single Component Angular Module (SCAM)."
},
"setup-mfe": {
"factory": "./src/generators/setup-mfe/setup-mfe.compat",
"schema": "./src/generators/setup-mfe/schema.json",
"description": "Generate a Module Federation configuration for a given Angular application."
},
"setup-tailwind": {
"factory": "./src/generators/setup-tailwind/compat",
"schema": "./src/generators/setup-tailwind/schema.json",
"description": "Configures TailwindCSS for an application or a buildable/publishable library."
},
"stories": {
"factory": "./src/generators/stories/compat",
"schema": "./src/generators/stories/schema.json",
@ -104,28 +129,6 @@
"schema": "./src/generators/upgrade-module/schema.json",
"description": "Sets up an Upgrade Module."
},
"setup-mfe": {
"factory": "./src/generators/setup-mfe/setup-mfe.compat",
"schema": "./src/generators/setup-mfe/schema.json",
"description": "Generate a Module Federation configuration for a given Angular application."
},
"scam": {
"factory": "./src/generators/scam/scam.compat",
"schema": "./src/generators/scam/schema.json",
"description": "Generate a component with an accompanying Single Component Angular Module (SCAM)."
},
"scam-directive": {
"factory": "./src/generators/scam-directive/scam-directive.compat",
"schema": "./src/generators/scam-directive/schema.json",
"description": "Generate a directive with an accompanying Single Component Angular Module (SCAM)."
},
"scam-pipe": {
"factory": "./src/generators/scam-pipe/scam-pipe.compat",
"schema": "./src/generators/scam-pipe/schema.json",
"description": "Generate a pipe with an accompanying Single Component Angular Module (SCAM)."
},
"web-worker": {
"factory": "./src/generators/web-worker/compat",
"schema": "./src/generators/web-worker/schema.json",
@ -146,11 +149,6 @@
"x-type": "application",
"description": "Creates an Angular application."
},
"setup-mfe": {
"factory": "./src/generators/setup-mfe/setup-mfe",
"schema": "./src/generators/setup-mfe/schema.json",
"description": "Generate a Module Federation configuration for a given Angular application."
},
"component-cypress-spec": {
"factory": "./src/generators/component-cypress-spec/component-cypress-spec",
"schema": "./src/generators/component-cypress-spec/schema.json",
@ -229,6 +227,16 @@
"schema": "./src/generators/scam-pipe/schema.json",
"description": "Generate a pipe with an accompanying Single Component Angular Module (SCAM)."
},
"setup-mfe": {
"factory": "./src/generators/setup-mfe/setup-mfe",
"schema": "./src/generators/setup-mfe/schema.json",
"description": "Generate a Module Federation configuration for a given Angular application."
},
"setup-tailwind": {
"factory": "./src/generators/setup-tailwind/setup-tailwind",
"schema": "./src/generators/setup-tailwind/schema.json",
"description": "Configures TailwindCSS for an application or a buildable/publishable library."
},
"stories": {
"factory": "./src/generators/stories/stories",
"schema": "./src/generators/stories/schema.json",

View File

@ -8,6 +8,7 @@ export * from './src/generators/library/library';
export * from './src/generators/library-secondary-entry-point/library-secondary-entry-point';
export * from './src/generators/move/move';
export * from './src/generators/ngrx/ngrx';
export * from './src/generators/setup-tailwind/setup-tailwind';
export * from './src/generators/stories/stories';
export * from './src/generators/storybook-configuration/storybook-configuration';
export * from './src/generators/storybook-migrate-defaults-5-to-6/storybook-migrate-defaults-5-to-6';

View File

@ -15,7 +15,8 @@
"webpack-merge",
"find-parent-dir",
"ts-node",
"tsconfig-paths"
"tsconfig-paths",
"semver"
],
"keepLifecycleScripts": true
}

View File

@ -49,6 +49,7 @@
"webpack-merge": "5.7.3",
"find-parent-dir": "^0.3.1",
"ts-node": "~9.1.1",
"tsconfig-paths": "^3.9.0"
"tsconfig-paths": "^3.9.0",
"semver": "7.3.4"
}
}

View File

@ -10,6 +10,11 @@ import {
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { Linter } from '@nrwl/linter';
import { E2eTestRunner, UnitTestRunner } from '../../utils/test-runners';
import {
autoprefixerVersion,
postcssVersion,
tailwindVersion,
} from '../../utils/versions';
import { applicationGenerator } from './application';
import type { Schema } from './schema';
@ -929,6 +934,63 @@ describe('app', () => {
expect(projectConfig.targets.serve.options.port).toBe(4205);
});
});
describe('--add-tailwind', () => {
it('should not add a tailwind.config.js and relevant packages when "--add-tailwind" is not specified', async () => {
// ACT
await generateApp(appTree, 'app1');
// ASSERT
expect(appTree.exists('apps/app1/tailwind.config.js')).toBeFalsy();
const { devDependencies } = readJson(appTree, 'package.json');
expect(devDependencies['tailwindcss']).toBeUndefined();
expect(devDependencies['postcss']).toBeUndefined();
expect(devDependencies['autoprefixer']).toBeUndefined();
});
it('should not add a tailwind.config.js and relevant packages when "--add-tailwind=false"', async () => {
// ACT
await generateApp(appTree, 'app1', { addTailwind: false });
// ASSERT
expect(appTree.exists('apps/app1/tailwind.config.js')).toBeFalsy();
const { devDependencies } = readJson(appTree, 'package.json');
expect(devDependencies['tailwindcss']).toBeUndefined();
expect(devDependencies['postcss']).toBeUndefined();
expect(devDependencies['autoprefixer']).toBeUndefined();
});
it('should add a tailwind.config.js and relevant packages when "--add-tailwind=true"', async () => {
// ACT
await generateApp(appTree, 'app1', { addTailwind: true });
// ASSERT
expect(appTree.read('apps/app1/tailwind.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"const { createGlobPatternsForDependencies } = require('@nrwl/angular/tailwind');
const { join } = require('path');
module.exports = {
content: [
join(__dirname, 'src/**/*.{html,ts}'),
...createGlobPatternsForDependencies(__dirname),
],
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
};
"
`);
const { devDependencies } = readJson(appTree, 'package.json');
expect(devDependencies['tailwindcss']).toBe(tailwindVersion);
expect(devDependencies['postcss']).toBe(postcssVersion);
expect(devDependencies['autoprefixer']).toBe(autoprefixerVersion);
});
});
});
async function generateApp(

View File

@ -1,36 +1,34 @@
import type { Schema } from './schema';
import {
readJson,
getWorkspacePath,
moveFilesToNewDirectory,
formatFiles,
getWorkspacePath,
installPackagesTask,
moveFilesToNewDirectory,
readJson,
Tree,
} from '@nrwl/devkit';
import { wrapAngularDevkitSchematic } from '@nrwl/devkit/ngcli-adapter';
import { convertToNxProjectGenerator } from '@nrwl/workspace';
import { UnitTestRunner } from '../../utils/test-runners';
import init from '../init/init';
import { angularInitGenerator } from '../init/init';
import { setupTailwindGenerator } from '../setup-tailwind/setup-tailwind';
import {
createFiles,
normalizeOptions,
updateAppComponentTemplate,
updateNxComponentTemplate,
updateConfigFiles,
addE2e,
updateComponentSpec,
addRouterRootConfiguration,
updateEditorTsConfig,
addProxyConfig,
enableStrictTypeChecking,
setApplicationStrictDefault,
addLinting,
addMfe,
addProxyConfig,
addRouterRootConfiguration,
addUnitTestRunner,
createFiles,
enableStrictTypeChecking,
normalizeOptions,
setApplicationStrictDefault,
updateAppComponentTemplate,
updateComponentSpec,
updateConfigFiles,
updateEditorTsConfig,
updateNxComponentTemplate,
} from './lib';
import { addUnitTestRunner } from './lib/add-unit-test-runner';
import type { Schema } from './schema';
export async function applicationGenerator(
host: Tree,
@ -52,7 +50,7 @@ export async function applicationGenerator(
? `${newProjectRoot}/${options.e2eProjectName}`
: `${options.name}/e2e`;
await init(host, {
await angularInitGenerator(host, {
...options,
skipFormat: true,
});
@ -97,6 +95,13 @@ export async function applicationGenerator(
});
updateNxComponentTemplate(host, options);
if (options.addTailwind) {
await setupTailwindGenerator(host, {
project: options.name,
skipFormat: true,
});
}
if (options.unitTestRunner !== UnitTestRunner.None) {
updateComponentSpec(host, options);
}

View File

@ -3,6 +3,7 @@ export * from './add-linting';
export * from './add-mfe';
export * from './add-protractor';
export * from './add-proxy-config';
export * from './add-unit-test-runner';
export * from './create-files';
export * from './enable-strict-type-checking';
export * from './normalize-options';

View File

@ -1,7 +1,11 @@
import type { Schema } from '../schema';
import { E2eTestRunner, UnitTestRunner } from '../../../utils/test-runners';
import type { AngularLinter, Schema } from '../schema';
export interface NormalizedSchema extends Schema {
prefix: string; // we set a default for this in normalizeOptions, so it is no longer optional
linter: AngularLinter;
unitTestRunner: UnitTestRunner;
e2eTestRunner: E2eTestRunner;
prefix: string;
appProjectRoot: string;
e2eProjectName: string;
e2eProjectRoot: string;

View File

@ -2,9 +2,12 @@ import { Linter } from '@nrwl/linter';
import { E2eTestRunner, UnitTestRunner } from '../../utils/test-runners';
import type { Styles } from '../utils/types';
type AngularLinter = Exclude<Linter, Linter.TsLint>;
export interface Schema {
name: string;
skipFormat: boolean;
addTailwind?: boolean;
skipFormat?: boolean;
inlineStyle?: boolean;
inlineTemplate?: boolean;
viewEncapsulation?: 'Emulated' | 'Native' | 'None';
@ -14,9 +17,9 @@ export interface Schema {
skipTests?: boolean;
directory?: string;
tags?: string;
linter: Exclude<Linter, Linter.TsLint>;
unitTestRunner: UnitTestRunner;
e2eTestRunner: E2eTestRunner;
linter?: AngularLinter;
unitTestRunner?: UnitTestRunner;
e2eTestRunner?: E2eTestRunner;
backendProject?: string;
strict?: boolean;
standaloneConfig?: boolean;

View File

@ -154,7 +154,13 @@
"type": "boolean",
"description": "Whether or not to configure the ESLint \"parserOptions.project\" option. We do not do this by default for lint performance reasons.",
"default": false
},
"addTailwind": {
"type": "boolean",
"description": "Whether to configure TailwindCSS for the application.",
"default": false
}
},
"required": []
"additionalProperties": false,
"required": ["name"]
}

View File

@ -51,7 +51,6 @@ function setDefaults(host: Tree, options: Schema) {
...(workspace.generators['@nrwl/angular:application'] || {}),
};
workspace.generators['@nrwl/angular:library'] = {
style: options.style,
linter: options.linter,
unitTestRunner: options.unitTestRunner,
...(workspace.generators['@nrwl/angular:library'] || {}),

View File

@ -5,7 +5,7 @@ import type { Styles } from '../utils/types';
export interface Schema {
unitTestRunner: UnitTestRunner;
e2eTestRunner?: E2eTestRunner;
skipFormat: boolean;
skipFormat?: boolean;
skipInstall?: boolean;
style?: Styles;
linter: Exclude<Linter, Linter.TsLint>;

View File

@ -52,6 +52,8 @@ export function normalizeOptions(
return {
...options,
linter: options.linter ?? Linter.EsLint,
unitTestRunner: options.unitTestRunner ?? UnitTestRunner.Jest,
prefix: options.prefix ?? defaultPrefix,
name: projectName,
projectRoot,

View File

@ -1,7 +1,10 @@
import { Schema } from '../schema';
import { UnitTestRunner } from '../../../utils/test-runners';
import type { AngularLinter, Schema } from '../schema';
export interface NormalizedSchema extends Schema {
name: string;
linter: AngularLinter;
unitTestRunner: UnitTestRunner;
prefix: string;
fileName: string;
projectRoot: string;
entryFile: string;
@ -9,5 +12,4 @@ export interface NormalizedSchema extends Schema {
moduleName: string;
projectDirectory: string;
parsedTags: string[];
prefix: string; // we set a default for this in normalizeOptions, so it is no longer optional
}

View File

@ -1,25 +1,30 @@
import {
readJson,
Tree,
updateJson,
parseJson,
getProjects,
NxJsonConfiguration,
parseJson,
readJson,
readProjectConfiguration,
Tree,
updateJson,
} from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { Linter } from '@nrwl/linter';
import { toNewFormat } from '@nrwl/tao/src/shared/workspace';
import { createApp } from '../../utils/nx-devkit/testing';
import { UnitTestRunner } from '../../utils/test-runners';
import {
autoprefixerVersion,
postcssVersion,
tailwindVersion,
} from '../../utils/versions';
import libraryGenerator from './library';
import { Schema } from './schema';
import { UnitTestRunner } from '../../utils/test-runners';
import { toNewFormat } from '@nrwl/tao/src/shared/workspace';
describe('lib', () => {
let appTree: Tree;
let tree: Tree;
async function runLibraryGeneratorWithOpts(opts: Partial<Schema> = {}) {
await libraryGenerator(appTree, {
await libraryGenerator(tree, {
name: 'myLib',
publishable: false,
buildable: false,
@ -33,20 +38,20 @@ describe('lib', () => {
}
beforeEach(() => {
appTree = createTreeWithEmptyWorkspace();
tree = createTreeWithEmptyWorkspace();
});
describe('workspace v2', () => {
beforeEach(() => {
appTree = createTreeWithEmptyWorkspace(2);
tree = createTreeWithEmptyWorkspace(2);
});
it('should default to standalone project for first project', async () => {
await runLibraryGeneratorWithOpts();
const workspaceJsonEntry = readJson(appTree, 'workspace.json').projects[
const workspaceJsonEntry = readJson(tree, 'workspace.json').projects[
'my-lib'
];
const projectConfig = readProjectConfiguration(appTree, 'my-lib');
const projectConfig = readProjectConfiguration(tree, 'my-lib');
expect(projectConfig.root).toEqual('libs/my-lib');
expect(workspaceJsonEntry).toEqual('libs/my-lib');
});
@ -55,10 +60,10 @@ describe('lib', () => {
await runLibraryGeneratorWithOpts({
standaloneConfig: false,
});
const workspaceJsonEntry = readJson(appTree, 'workspace.json').projects[
const workspaceJsonEntry = readJson(tree, 'workspace.json').projects[
'my-lib'
];
const projectConfig = readProjectConfiguration(appTree, 'my-lib');
const projectConfig = readProjectConfiguration(tree, 'my-lib');
expect(projectConfig.root).toEqual('libs/my-lib');
expect(projectConfig).toMatchObject(workspaceJsonEntry);
});
@ -66,17 +71,16 @@ describe('lib', () => {
describe('workspace v1', () => {
beforeEach(() => {
appTree = createTreeWithEmptyWorkspace(1);
tree = createTreeWithEmptyWorkspace(1);
});
it('should default to inline project for first project', async () => {
await runLibraryGeneratorWithOpts({
standaloneConfig: false,
});
const workspaceJsonEntry = toNewFormat(
readJson(appTree, 'workspace.json')
).projects['my-lib'];
const projectConfig = readProjectConfiguration(appTree, 'my-lib');
const workspaceJsonEntry = toNewFormat(readJson(tree, 'workspace.json'))
.projects['my-lib'];
const projectConfig = readProjectConfiguration(tree, 'my-lib');
expect(projectConfig.root).toEqual('libs/my-lib');
expect(projectConfig).toMatchObject(workspaceJsonEntry);
});
@ -98,7 +102,7 @@ describe('lib', () => {
});
// ASSERT
let ngPackage = readJson(appTree, 'libs/my-lib/ng-package.json');
let ngPackage = readJson(tree, 'libs/my-lib/ng-package.json');
expect(ngPackage.dest).toEqual('../../dist/libs/my-lib');
});
@ -110,7 +114,7 @@ describe('lib', () => {
});
// ASSERT
let ngPackage = readJson(appTree, 'libs/my-lib/ng-package.json');
let ngPackage = readJson(tree, 'libs/my-lib/ng-package.json');
expect(ngPackage.$schema).toEqual(
'../../node_modules/ng-packagr/ng-package.schema.json'
@ -122,7 +126,7 @@ describe('lib', () => {
await runLibraryGeneratorWithOpts();
// ASSERT
const packageJson = readJson(appTree, '/package.json');
const packageJson = readJson(tree, '/package.json');
expect(packageJson.devDependencies['ng-packagr']).toBeUndefined();
expect(packageJson.devDependencies['postcss']).toBeUndefined();
expect(packageJson.devDependencies['postcss-import']).toBeUndefined();
@ -138,7 +142,7 @@ describe('lib', () => {
});
// ASSERT
const packageJson = readJson(appTree, '/package.json');
const packageJson = readJson(tree, '/package.json');
expect(packageJson.devDependencies['ng-packagr']).toBeDefined();
expect(packageJson.devDependencies['postcss']).toBeDefined();
expect(packageJson.devDependencies['postcss-import']).toBeDefined();
@ -151,7 +155,7 @@ describe('lib', () => {
await runLibraryGeneratorWithOpts({ buildable: true });
// ASSERT
const packageJson = readJson(appTree, '/package.json');
const packageJson = readJson(tree, '/package.json');
expect(packageJson.devDependencies['ng-packagr']).toBeDefined();
expect(packageJson.devDependencies['postcss']).toBeDefined();
expect(packageJson.devDependencies['postcss-import']).toBeDefined();
@ -167,7 +171,7 @@ describe('lib', () => {
});
// ASSERT
const workspaceJson = readJson(appTree, '/workspace.json');
const workspaceJson = readJson(tree, '/workspace.json');
expect(workspaceJson.projects['my-lib'].root).toEqual('libs/my-lib');
expect(workspaceJson.projects['my-lib'].architect.build).toBeDefined();
@ -180,7 +184,7 @@ describe('lib', () => {
});
// ASSERT
const workspaceJson = readJson(appTree, '/workspace.json');
const workspaceJson = readJson(tree, '/workspace.json');
expect(workspaceJson.projects['my-lib'].root).toEqual('libs/my-lib');
expect(
@ -196,7 +200,7 @@ describe('lib', () => {
});
// ASSERT
const workspaceJson = readJson(appTree, '/workspace.json');
const workspaceJson = readJson(tree, '/workspace.json');
expect(workspaceJson.projects['my-lib'].root).toEqual('libs/my-lib');
expect(workspaceJson.projects['my-lib'].architect.build).toBeDefined();
@ -210,7 +214,7 @@ describe('lib', () => {
});
// ASSERT
const libProdConfig = appTree.read('libs/my-lib/tsconfig.lib.prod.json');
const libProdConfig = tree.read('libs/my-lib/tsconfig.lib.prod.json');
expect(libProdConfig).toBeFalsy();
});
@ -224,7 +228,7 @@ describe('lib', () => {
});
// ASSERT
const projects = Object.fromEntries(getProjects(appTree));
const projects = Object.fromEntries(getProjects(tree));
expect(projects).toEqual({
'my-lib': expect.objectContaining({
tags: ['one', 'two'],
@ -237,7 +241,7 @@ describe('lib', () => {
await runLibraryGeneratorWithOpts();
// ASSERT
const tsconfigJson = readJson(appTree, '/tsconfig.base.json');
const tsconfigJson = readJson(tree, '/tsconfig.base.json');
expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([
'libs/my-lib/src/index.ts',
]);
@ -248,7 +252,7 @@ describe('lib', () => {
await runLibraryGeneratorWithOpts();
// ASSERT
const tsconfigJson = readJson(appTree, 'libs/my-lib/tsconfig.json');
const tsconfigJson = readJson(tree, 'libs/my-lib/tsconfig.json');
expect(tsconfigJson).toEqual({
extends: '../../tsconfig.base.json',
angularCompilerOptions: {
@ -280,7 +284,7 @@ describe('lib', () => {
it('should check for existence of spec files before deleting them', async () => {
// ARRANGE
updateJson<NxJsonConfiguration, NxJsonConfiguration>(
appTree,
tree,
'/nx.json',
(nxJson) => {
nxJson.generators = {
@ -301,10 +305,10 @@ describe('lib', () => {
// ASSERT
expect(
appTree.read('libs/my-lib/src/lib/my-lib.component.spec.ts')
tree.read('libs/my-lib/src/lib/my-lib.component.spec.ts')
).toBeFalsy();
expect(
appTree.read('libs/my-lib/src/lib/my-lib.service.spec.ts')
tree.read('libs/my-lib/src/lib/my-lib.service.spec.ts')
).toBeFalsy();
});
@ -313,7 +317,7 @@ describe('lib', () => {
await runLibraryGeneratorWithOpts();
// ASSERT
const tsconfigJson = readJson(appTree, 'libs/my-lib/tsconfig.spec.json');
const tsconfigJson = readJson(tree, 'libs/my-lib/tsconfig.spec.json');
expect(tsconfigJson.extends).toEqual('./tsconfig.json');
});
@ -323,7 +327,7 @@ describe('lib', () => {
await runLibraryGeneratorWithOpts();
// ASSERT
const tsconfigJson = readJson(appTree, 'libs/my-lib/tsconfig.lib.json');
const tsconfigJson = readJson(tree, 'libs/my-lib/tsconfig.lib.json');
expect(tsconfigJson.extends).toEqual('./tsconfig.json');
});
@ -332,7 +336,7 @@ describe('lib', () => {
await runLibraryGeneratorWithOpts();
// ASSERT
const tsConfigJson = readJson(appTree, 'libs/my-lib/tsconfig.lib.json');
const tsConfigJson = readJson(tree, 'libs/my-lib/tsconfig.lib.json');
expect(tsConfigJson.include).toEqual(['**/*.ts']);
});
@ -341,7 +345,7 @@ describe('lib', () => {
await runLibraryGeneratorWithOpts();
// ASSERT
const tsconfigJson = readJson(appTree, 'libs/my-lib/tsconfig.lib.json');
const tsconfigJson = readJson(tree, 'libs/my-lib/tsconfig.lib.json');
expect(tsconfigJson.exclude).toEqual([
'src/test-setup.ts',
'**/*.spec.ts',
@ -356,7 +360,7 @@ describe('lib', () => {
});
// ASSERT
const tsconfigJson = readJson(appTree, 'libs/my-lib/tsconfig.lib.json');
const tsconfigJson = readJson(tree, 'libs/my-lib/tsconfig.lib.json');
expect(tsconfigJson.exclude).toEqual([
'src/test.ts',
'**/*.spec.ts',
@ -371,7 +375,7 @@ describe('lib', () => {
});
// ASSERT
const tsconfigJson = readJson(appTree, 'libs/my-lib/tsconfig.lib.json');
const tsconfigJson = readJson(tree, 'libs/my-lib/tsconfig.lib.json');
expect(tsconfigJson.exclude).toEqual(['**/*.test.ts', '**/*.spec.ts']);
});
});
@ -382,42 +386,38 @@ describe('lib', () => {
await runLibraryGeneratorWithOpts({ name: 'my-lib2' });
// ASSERT
expect(appTree.exists(`libs/my-lib/jest.config.js`)).toBeTruthy();
expect(appTree.exists('libs/my-lib/src/index.ts')).toBeTruthy();
expect(tree.exists(`libs/my-lib/jest.config.js`)).toBeTruthy();
expect(tree.exists('libs/my-lib/src/index.ts')).toBeTruthy();
expect(tree.exists('libs/my-lib/src/lib/my-lib.module.ts')).toBeTruthy();
expect(
appTree.exists('libs/my-lib/src/lib/my-lib.module.ts')
tree.exists('libs/my-lib/src/lib/my-lib.component.ts')
).toBeFalsy();
expect(
tree.exists('libs/my-lib/src/lib/my-lib.component.spec.ts')
).toBeFalsy();
expect(tree.exists('libs/my-lib/src/lib/my-lib.service.ts')).toBeFalsy();
expect(
tree.exists('libs/my-lib/src/lib/my-lib.service.spec.ts')
).toBeFalsy();
expect(tree.exists(`libs/my-lib2/jest.config.js`)).toBeTruthy();
expect(tree.exists('libs/my-lib2/src/index.ts')).toBeTruthy();
expect(
tree.exists('libs/my-lib2/src/lib/my-lib2.module.ts')
).toBeTruthy();
expect(
appTree.exists('libs/my-lib/src/lib/my-lib.component.ts')
tree.exists('libs/my-lib2/src/lib/my-lib2.component.ts')
).toBeFalsy();
expect(
appTree.exists('libs/my-lib/src/lib/my-lib.component.spec.ts')
tree.exists('libs/my-lib2/src/lib/my-lib2.component.spec.ts')
).toBeFalsy();
expect(
appTree.exists('libs/my-lib/src/lib/my-lib.service.ts')
tree.exists('libs/my-lib2/src/lib/my-lib2.service.ts')
).toBeFalsy();
expect(
appTree.exists('libs/my-lib/src/lib/my-lib.service.spec.ts')
).toBeFalsy();
expect(appTree.exists(`libs/my-lib2/jest.config.js`)).toBeTruthy();
expect(appTree.exists('libs/my-lib2/src/index.ts')).toBeTruthy();
expect(
appTree.exists('libs/my-lib2/src/lib/my-lib2.module.ts')
).toBeTruthy();
expect(
appTree.exists('libs/my-lib2/src/lib/my-lib2.component.ts')
).toBeFalsy();
expect(
appTree.exists('libs/my-lib2/src/lib/my-lib2.component.spec.ts')
).toBeFalsy();
expect(
appTree.exists('libs/my-lib2/src/lib/my-lib2.service.ts')
).toBeFalsy();
expect(
appTree.exists('libs/my-lib2/src/lib/my-lib2.service.spec.ts')
tree.exists('libs/my-lib2/src/lib/my-lib2.service.spec.ts')
).toBeFalsy();
});
@ -431,12 +431,12 @@ describe('lib', () => {
// ASSERT
expect(
JSON.parse(appTree.read('workspace.json').toString()).projects['my-lib']
JSON.parse(tree.read('workspace.json').toString()).projects['my-lib']
.prefix
).toEqual('proj');
expect(
JSON.parse(appTree.read('workspace.json').toString()).projects[
JSON.parse(tree.read('workspace.json').toString()).projects[
'my-lib-with-prefix'
].prefix
).toEqual('custom');
@ -454,7 +454,7 @@ describe('lib', () => {
});
// ASSERT
const projects = Object.fromEntries(getProjects(appTree));
const projects = Object.fromEntries(getProjects(tree));
expect(projects).toEqual({
'my-dir-my-lib': expect.objectContaining({
@ -476,42 +476,42 @@ describe('lib', () => {
});
// ASSERT
expect(appTree.exists(`libs/my-dir/my-lib/jest.config.js`)).toBeTruthy();
expect(appTree.exists('libs/my-dir/my-lib/src/index.ts')).toBeTruthy();
expect(tree.exists(`libs/my-dir/my-lib/jest.config.js`)).toBeTruthy();
expect(tree.exists('libs/my-dir/my-lib/src/index.ts')).toBeTruthy();
expect(
appTree.exists('libs/my-dir/my-lib/src/lib/my-dir-my-lib.module.ts')
tree.exists('libs/my-dir/my-lib/src/lib/my-dir-my-lib.module.ts')
).toBeTruthy();
expect(
appTree.exists('libs/my-dir/my-lib/src/lib/my-lib.component.ts')
tree.exists('libs/my-dir/my-lib/src/lib/my-lib.component.ts')
).toBeFalsy();
expect(
appTree.exists('libs/my-dir/my-lib/src/lib/my-lib.component.spec.ts')
tree.exists('libs/my-dir/my-lib/src/lib/my-lib.component.spec.ts')
).toBeFalsy();
expect(
appTree.exists('libs/my-dir/my-lib/src/lib/my-lib.service.ts')
tree.exists('libs/my-dir/my-lib/src/lib/my-lib.service.ts')
).toBeFalsy();
expect(
appTree.exists('libs/my-dir/my-lib/src/lib/my-lib.service.spec.ts')
tree.exists('libs/my-dir/my-lib/src/lib/my-lib.service.spec.ts')
).toBeFalsy();
expect(appTree.exists(`libs/my-dir/my-lib2/jest.config.js`)).toBeTruthy();
expect(appTree.exists('libs/my-dir/my-lib2/src/index.ts')).toBeTruthy();
expect(tree.exists(`libs/my-dir/my-lib2/jest.config.js`)).toBeTruthy();
expect(tree.exists('libs/my-dir/my-lib2/src/index.ts')).toBeTruthy();
expect(
appTree.exists('libs/my-dir/my-lib2/src/lib/my-lib2.module.ts')
tree.exists('libs/my-dir/my-lib2/src/lib/my-lib2.module.ts')
).toBeTruthy();
expect(
appTree.exists('libs/my-dir/my-lib2/src/lib/my-lib2.component.ts')
tree.exists('libs/my-dir/my-lib2/src/lib/my-lib2.component.ts')
).toBeFalsy();
expect(
appTree.exists('libs/my-dir/my-lib2/src/lib/my-lib2.component.spec.ts')
tree.exists('libs/my-dir/my-lib2/src/lib/my-lib2.component.spec.ts')
).toBeFalsy();
expect(
appTree.exists('libs/my-dir/my-lib2/src/lib/my-lib2.service.ts')
tree.exists('libs/my-dir/my-lib2/src/lib/my-lib2.service.ts')
).toBeFalsy();
expect(
appTree.exists('libs/my-dir/my-lib2/src/lib/my-lib2.service.spec.ts')
tree.exists('libs/my-dir/my-lib2/src/lib/my-lib2.service.spec.ts')
).toBeFalsy();
});
@ -524,7 +524,7 @@ describe('lib', () => {
});
// ASSERT
let ngPackage = readJson(appTree, 'libs/my-dir/my-lib/ng-package.json');
let ngPackage = readJson(tree, 'libs/my-dir/my-lib/ng-package.json');
expect(ngPackage.dest).toEqual('../../../dist/libs/my-dir/my-lib');
});
@ -533,7 +533,7 @@ describe('lib', () => {
await runLibraryGeneratorWithOpts({ directory: 'myDir' });
// ASSERT
const workspaceJson = readJson(appTree, '/workspace.json');
const workspaceJson = readJson(tree, '/workspace.json');
expect(workspaceJson.projects['my-dir-my-lib'].root).toEqual(
'libs/my-dir/my-lib'
@ -545,7 +545,7 @@ describe('lib', () => {
await runLibraryGeneratorWithOpts({ directory: 'myDir' });
// ASSERT
const tsconfigJson = readJson(appTree, '/tsconfig.base.json');
const tsconfigJson = readJson(tree, '/tsconfig.base.json');
expect(tsconfigJson.compilerOptions.paths['@proj/my-dir/my-lib']).toEqual(
['libs/my-dir/my-lib/src/index.ts']
);
@ -556,7 +556,7 @@ describe('lib', () => {
it('should update tsconfig.json (no existing path mappings)', async () => {
// ARRANGE
updateJson(appTree, 'tsconfig.base.json', (json) => {
updateJson(tree, 'tsconfig.base.json', (json) => {
json.compilerOptions.paths = undefined;
return json;
});
@ -565,7 +565,7 @@ describe('lib', () => {
await runLibraryGeneratorWithOpts({ directory: 'myDir' });
// ASSERT
const tsconfigJson = readJson(appTree, '/tsconfig.base.json');
const tsconfigJson = readJson(tree, '/tsconfig.base.json');
expect(tsconfigJson.compilerOptions.paths['@proj/my-dir/my-lib']).toEqual(
['libs/my-dir/my-lib/src/index.ts']
@ -580,10 +580,7 @@ describe('lib', () => {
await runLibraryGeneratorWithOpts({ directory: 'myDir' });
// ASSERT
const tsconfigJson = readJson(
appTree,
'libs/my-dir/my-lib/tsconfig.json'
);
const tsconfigJson = readJson(tree, 'libs/my-dir/my-lib/tsconfig.json');
expect(tsconfigJson).toEqual({
extends: '../../../tsconfig.base.json',
@ -616,8 +613,8 @@ describe('lib', () => {
describe('at the root', () => {
beforeEach(() => {
appTree = createTreeWithEmptyWorkspace(2);
updateJson(appTree, 'nx.json', (json) => ({
tree = createTreeWithEmptyWorkspace(2);
updateJson(tree, 'nx.json', (json) => ({
...json,
workspaceLayout: { libsDir: '' },
}));
@ -627,7 +624,7 @@ describe('lib', () => {
await runLibraryGeneratorWithOpts({ directory: 'src/1-api' });
// ASSERT
const workspaceJson = readJson(appTree, '/workspace.json');
const workspaceJson = readJson(tree, '/workspace.json');
expect(workspaceJson.projects['src-api-my-lib']).toEqual(
'src/1-api/my-lib'
@ -636,17 +633,17 @@ describe('lib', () => {
it('should have root relative routes', async () => {
await runLibraryGeneratorWithOpts({ directory: 'myDir' });
const workspaceJsonEntry = readJson(appTree, 'workspace.json').projects[
const workspaceJsonEntry = readJson(tree, 'workspace.json').projects[
'my-dir-my-lib'
];
const projectConfig = readProjectConfiguration(appTree, 'my-dir-my-lib');
const projectConfig = readProjectConfiguration(tree, 'my-dir-my-lib');
expect(projectConfig.root).toEqual('my-dir/my-lib');
expect(workspaceJsonEntry).toEqual('my-dir/my-lib');
});
it('should generate files with correct output paths', async () => {
const hasJsonValue = ({ path, expectedValue, lookupFn }) => {
const content = readJson(appTree, path);
const content = readJson(tree, path);
expect(lookupFn(content)).toEqual(expectedValue);
};
@ -658,9 +655,7 @@ describe('lib', () => {
});
const libModulePath = 'my-dir/my-lib/src/lib/my-lib.module.ts';
expect(appTree.read(libModulePath, 'utf-8')).toContain(
'class MyLibModule'
);
expect(tree.read(libModulePath, 'utf-8')).toContain('class MyLibModule');
// Make sure these exist
[
@ -671,7 +666,7 @@ describe('lib', () => {
'my-dir/my-lib/src/index.ts',
'my-dir/my-lib/src/lib/my-lib.module.ts',
].forEach((path) => {
expect(appTree.exists(path)).toBeTruthy();
expect(tree.exists(path)).toBeTruthy();
});
// Make sure these have properties
@ -707,17 +702,10 @@ describe('lib', () => {
describe('router', () => {
it('should error when lazy is set without routing', async () => {
try {
// ACT
await runLibraryGeneratorWithOpts({ lazy: true });
fail();
} catch (e) {
// ASSERT
expect(e.message).toEqual(
'To use --lazy option, --routing must also be set.'
);
}
// ACT & ASSERT
await expect(runLibraryGeneratorWithOpts({ lazy: true })).rejects.toThrow(
'To use "--lazy" option, "--routing" must also be set.'
);
});
describe('lazy', () => {
@ -739,27 +727,25 @@ describe('lib', () => {
// ASSERT
expect(
appTree.exists('libs/my-dir/my-lib/src/lib/my-dir-my-lib.module.ts')
tree.exists('libs/my-dir/my-lib/src/lib/my-dir-my-lib.module.ts')
).toBeTruthy();
expect(
appTree
tree
.read('libs/my-dir/my-lib/src/lib/my-dir-my-lib.module.ts')
.toString()
).toContain('RouterModule.forChild');
expect(
appTree.exists('libs/my-dir/my-lib2/src/lib/my-lib2.module.ts')
tree.exists('libs/my-dir/my-lib2/src/lib/my-lib2.module.ts')
).toBeTruthy();
expect(
appTree
.read('libs/my-dir/my-lib2/src/lib/my-lib2.module.ts')
.toString()
tree.read('libs/my-dir/my-lib2/src/lib/my-lib2.module.ts').toString()
).toContain('RouterModule.forChild');
});
it('should update the parent module', async () => {
// ARRANGE
createApp(appTree, 'myapp');
createApp(tree, 'myapp');
// ACT
await runLibraryGeneratorWithOpts({
@ -769,16 +755,13 @@ describe('lib', () => {
parentModule: 'apps/myapp/src/app/app.module.ts',
});
const moduleContents = appTree
const moduleContents = tree
.read('apps/myapp/src/app/app.module.ts')
.toString();
const tsConfigAppJson = readJson(
appTree,
'apps/myapp/tsconfig.app.json'
);
const tsConfigAppJson = readJson(tree, 'apps/myapp/tsconfig.app.json');
const tsConfigLibJson = parseJson(
appTree.read('libs/my-dir/my-lib/tsconfig.lib.json').toString()
tree.read('libs/my-dir/my-lib/tsconfig.lib.json').toString()
);
await runLibraryGeneratorWithOpts({
@ -790,15 +773,15 @@ describe('lib', () => {
parentModule: 'apps/myapp/src/app/app.module.ts',
});
const moduleContents2 = appTree
const moduleContents2 = tree
.read('apps/myapp/src/app/app.module.ts')
.toString();
const tsConfigAppJson2 = parseJson(
appTree.read('apps/myapp/tsconfig.app.json').toString()
tree.read('apps/myapp/tsconfig.app.json').toString()
);
const tsConfigLibJson2 = parseJson(
appTree.read('libs/my-dir/my-lib2/tsconfig.lib.json').toString()
tree.read('libs/my-dir/my-lib2/tsconfig.lib.json').toString()
);
await runLibraryGeneratorWithOpts({
@ -810,16 +793,16 @@ describe('lib', () => {
parentModule: 'apps/myapp/src/app/app.module.ts',
});
const moduleContents3 = appTree
const moduleContents3 = tree
.read('apps/myapp/src/app/app.module.ts')
.toString();
const tsConfigAppJson3 = parseJson(
appTree.read('apps/myapp/tsconfig.app.json').toString()
tree.read('apps/myapp/tsconfig.app.json').toString()
);
const tsConfigLibJson3 = parseJson(
appTree.read('libs/my-dir/my-lib3/tsconfig.lib.json').toString()
tree.read('libs/my-dir/my-lib3/tsconfig.lib.json').toString()
);
// ASSERT
@ -883,8 +866,8 @@ describe('lib', () => {
it('should update the parent module even if the route is declared outside the .forRoot(...)', async () => {
// ARRANGE
createApp(appTree, 'myapp');
appTree.write(
createApp(tree, 'myapp');
tree.write(
'apps/myapp/src/app/app.module.ts',
`
import { NgModule } from '@angular/core';
@ -912,7 +895,7 @@ describe('lib', () => {
});
// ASSERT
const moduleContents = appTree
const moduleContents = tree
.read('apps/myapp/src/app/app.module.ts')
.toString();
@ -939,37 +922,33 @@ describe('lib', () => {
});
// ASSERT
expect(
appTree.exists('libs/my-dir/my-lib/src/lib/my-dir-my-lib.module.ts')
tree.exists('libs/my-dir/my-lib/src/lib/my-dir-my-lib.module.ts')
).toBeTruthy();
expect(
appTree
tree
.read('libs/my-dir/my-lib/src/lib/my-dir-my-lib.module.ts')
.toString()
).toContain('RouterModule');
expect(
appTree
tree
.read('libs/my-dir/my-lib/src/lib/my-dir-my-lib.module.ts')
.toString()
).toContain('const myDirMyLibRoutes: Route[] = ');
expect(
appTree.exists('libs/my-dir/my-lib2/src/lib/my-lib2.module.ts')
tree.exists('libs/my-dir/my-lib2/src/lib/my-lib2.module.ts')
).toBeTruthy();
expect(
appTree
.read('libs/my-dir/my-lib2/src/lib/my-lib2.module.ts')
.toString()
tree.read('libs/my-dir/my-lib2/src/lib/my-lib2.module.ts').toString()
).toContain('RouterModule');
expect(
appTree
.read('libs/my-dir/my-lib2/src/lib/my-lib2.module.ts')
.toString()
tree.read('libs/my-dir/my-lib2/src/lib/my-lib2.module.ts').toString()
).toContain('const myLib2Routes: Route[] = ');
});
it('should update the parent module', async () => {
// ARRANGE
createApp(appTree, 'myapp');
createApp(tree, 'myapp');
// ACT
await runLibraryGeneratorWithOpts({
@ -979,7 +958,7 @@ describe('lib', () => {
parentModule: 'apps/myapp/src/app/app.module.ts',
});
const moduleContents = appTree
const moduleContents = tree
.read('apps/myapp/src/app/app.module.ts')
.toString();
@ -991,7 +970,7 @@ describe('lib', () => {
parentModule: 'apps/myapp/src/app/app.module.ts',
});
const moduleContents2 = appTree
const moduleContents2 = tree
.read('apps/myapp/src/app/app.module.ts')
.toString();
@ -1003,7 +982,7 @@ describe('lib', () => {
simpleModuleName: true,
});
const moduleContents3 = appTree
const moduleContents3 = tree
.read('apps/myapp/src/app/app.module.ts')
.toString();
@ -1038,8 +1017,8 @@ describe('lib', () => {
it('should update the parent module even if the route is declared outside the .forRoot(...)', async () => {
// ARRANGE
createApp(appTree, 'myapp');
appTree.write(
createApp(tree, 'myapp');
tree.write(
'apps/myapp/src/app/app.module.ts',
`
import { NgModule } from '@angular/core';
@ -1067,7 +1046,7 @@ describe('lib', () => {
});
// ASSERT
const moduleContents = appTree
const moduleContents = tree
.read('apps/myapp/src/app/app.module.ts')
.toString();
@ -1087,16 +1066,16 @@ describe('lib', () => {
});
// ASSERT
const workspaceJson = readJson(appTree, 'workspace.json');
const workspaceJson = readJson(tree, 'workspace.json');
expect(appTree.exists('libs/my-lib/src/test.ts')).toBeTruthy();
expect(appTree.exists('libs/my-lib/src/test-setup.ts')).toBeFalsy();
expect(appTree.exists('libs/my-lib/tsconfig.spec.json')).toBeTruthy();
expect(appTree.exists('libs/my-lib/karma.conf.js')).toBeTruthy();
expect(tree.exists('libs/my-lib/src/test.ts')).toBeTruthy();
expect(tree.exists('libs/my-lib/src/test-setup.ts')).toBeFalsy();
expect(tree.exists('libs/my-lib/tsconfig.spec.json')).toBeTruthy();
expect(tree.exists('libs/my-lib/karma.conf.js')).toBeTruthy();
expect(
appTree.exists('libs/my-lib/src/lib/my-lib.module.spec.ts')
tree.exists('libs/my-lib/src/lib/my-lib.module.spec.ts')
).toBeFalsy();
expect(appTree.exists('karma.conf.js')).toBeTruthy();
expect(tree.exists('karma.conf.js')).toBeTruthy();
expect(workspaceJson.projects['my-lib'].architect.test.builder).toEqual(
'@angular-devkit/build-angular:karma'
);
@ -1111,7 +1090,7 @@ describe('lib', () => {
// ASSERT
expect(
appTree.exists('libs/my-lib/src/lib/my-lib.module.spec.ts')
tree.exists('libs/my-lib/src/lib/my-lib.module.spec.ts')
).toBeTruthy();
});
});
@ -1124,16 +1103,16 @@ describe('lib', () => {
});
// ASSERT
const workspaceJson = readJson(appTree, 'workspace.json');
const workspaceJson = readJson(tree, 'workspace.json');
expect(
appTree.exists('libs/my-lib/src/lib/my-lib.module.spec.ts')
tree.exists('libs/my-lib/src/lib/my-lib.module.spec.ts')
).toBeFalsy();
expect(appTree.exists('libs/my-lib/src/test.ts')).toBeFalsy();
expect(appTree.exists('libs/my-lib/src/test.ts')).toBeFalsy();
expect(appTree.exists('libs/my-lib/tsconfig.spec.json')).toBeFalsy();
expect(appTree.exists('libs/my-lib/jest.config.js')).toBeFalsy();
expect(appTree.exists('libs/my-lib/karma.conf.js')).toBeFalsy();
expect(tree.exists('libs/my-lib/src/test.ts')).toBeFalsy();
expect(tree.exists('libs/my-lib/src/test.ts')).toBeFalsy();
expect(tree.exists('libs/my-lib/tsconfig.spec.json')).toBeFalsy();
expect(tree.exists('libs/my-lib/jest.config.js')).toBeFalsy();
expect(tree.exists('libs/my-lib/karma.conf.js')).toBeFalsy();
expect(workspaceJson.projects['my-lib'].architect.test).toBeUndefined();
});
});
@ -1148,8 +1127,8 @@ describe('lib', () => {
});
// ASSERT
const packageJson = readJson(appTree, 'libs/my-dir/my-lib/package.json');
const tsconfigJson = readJson(appTree, '/tsconfig.base.json');
const packageJson = readJson(tree, 'libs/my-dir/my-lib/package.json');
const tsconfigJson = readJson(tree, '/tsconfig.base.json');
expect(packageJson.name).toBe('@myorg/lib');
expect(
@ -1164,35 +1143,27 @@ describe('lib', () => {
importPath: '@myorg/lib',
});
try {
// ACT
await runLibraryGeneratorWithOpts({
// ACT & ASSERT
await expect(
runLibraryGeneratorWithOpts({
name: 'myLib2',
publishable: true,
importPath: '@myorg/lib',
});
} catch (e) {
expect(e.message).toContain(
'You already have a library using the import path'
);
}
expect.assertions(1);
})
).rejects.toThrowError(
'You already have a library using the import path'
);
});
it('should fail if no importPath has been used', async () => {
try {
// ACT
await runLibraryGeneratorWithOpts({
// ACT && ASSERT
await expect(
runLibraryGeneratorWithOpts({
publishable: true,
});
} catch (e) {
expect(e.message).toContain(
'For publishable libs you have to provide a proper "--importPath"'
);
}
expect.assertions(1);
})
).rejects.toThrowError(
'For publishable libs you have to provide a proper "--importPath"'
);
});
});
@ -1207,10 +1178,10 @@ describe('lib', () => {
// ASSERT
const { compilerOptions, angularCompilerOptions } = readJson(
appTree,
tree,
'libs/my-lib/tsconfig.json'
);
const { generators } = readJson<NxJsonConfiguration>(appTree, 'nx.json');
const { generators } = readJson<NxJsonConfiguration>(tree, 'nx.json');
// check that the TypeScript compiler options have been updated
expect(compilerOptions.forceConsistentCasingInFileNames).toBe(true);
@ -1240,7 +1211,7 @@ describe('lib', () => {
// ASSERT
// check to see if the workspace configuration has been updated to turn off
// strict mode by default in future libraries
const { generators } = readJson<NxJsonConfiguration>(appTree, 'nx.json');
const { generators } = readJson<NxJsonConfiguration>(tree, 'nx.json');
expect(generators['@nrwl/angular:library'].strict).toBe(false);
});
});
@ -1252,9 +1223,9 @@ describe('lib', () => {
await runLibraryGeneratorWithOpts({ linter: Linter.EsLint });
// ASSERT
const workspaceJson = readJson(appTree, 'workspace.json');
const workspaceJson = readJson(tree, 'workspace.json');
expect(appTree.exists('libs/my-lib/tslint.json')).toBe(false);
expect(tree.exists('libs/my-lib/tslint.json')).toBe(false);
expect(workspaceJson.projects['my-lib'].architect.lint)
.toMatchInlineSnapshot(`
Object {
@ -1275,7 +1246,7 @@ describe('lib', () => {
// ASSERT
const eslintConfig = readJson(appTree, 'libs/my-lib/.eslintrc.json');
const eslintConfig = readJson(tree, 'libs/my-lib/.eslintrc.json');
expect(eslintConfig).toMatchInlineSnapshot(`
Object {
"extends": Array [
@ -1333,9 +1304,72 @@ describe('lib', () => {
await runLibraryGeneratorWithOpts({ linter: Linter.None });
// ASSERT
const workspaceJson = readJson(appTree, 'workspace.json');
const workspaceJson = readJson(tree, 'workspace.json');
expect(workspaceJson.projects['my-lib'].architect.lint).toBeUndefined();
});
});
});
describe('--add-tailwind', () => {
it('should throw when "--addTailwind=true" and "--buildable" and "--publishable" are not set', async () => {
// ACT & ASSERT
await expect(
runLibraryGeneratorWithOpts({ addTailwind: true })
).rejects.toThrow(
`To use "--addTailwind" option, you have to set either "--buildable" or "--publishable".`
);
});
it('should not set up Tailwind when "--add-tailwind" is not specified', async () => {
// ACT
await runLibraryGeneratorWithOpts();
// ASSERT
expect(tree.exists('libs/my-lib/tailwind.config.js')).toBeFalsy();
const { devDependencies } = readJson(tree, 'package.json');
expect(devDependencies['tailwindcss']).toBeUndefined();
expect(devDependencies['postcss']).toBeUndefined();
expect(devDependencies['autoprefixer']).toBeUndefined();
});
it('should not set up Tailwind when "--add-tailwind=false"', async () => {
// ACT
await runLibraryGeneratorWithOpts({ addTailwind: false });
// ASSERT
expect(tree.exists('libs/my-lib/tailwind.config.js')).toBeFalsy();
const { devDependencies } = readJson(tree, 'package.json');
expect(devDependencies['tailwindcss']).toBeUndefined();
expect(devDependencies['postcss']).toBeUndefined();
expect(devDependencies['autoprefixer']).toBeUndefined();
});
it('should set up Tailwind when "--add-tailwind=true"', async () => {
// ACT
await runLibraryGeneratorWithOpts({ addTailwind: true, buildable: true });
// ASSERT
expect(tree.read('libs/my-lib/tailwind.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"module.exports = {
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
};
"
`);
const project = readProjectConfiguration(tree, 'my-lib');
expect(project.targets.build.options.tailwindConfig).toBe(
'libs/my-lib/tailwind.config.js'
);
const { devDependencies } = readJson(tree, 'package.json');
expect(devDependencies['tailwindcss']).toBe(tailwindVersion);
expect(devDependencies['postcss']).toBe(postcssVersion);
expect(devDependencies['autoprefixer']).toBe(autoprefixerVersion);
});
});
});

View File

@ -8,28 +8,28 @@ import {
import { wrapAngularDevkitSchematic } from '@nrwl/devkit/ngcli-adapter';
import { jestProjectGenerator } from '@nrwl/jest';
import { Linter } from '@nrwl/linter';
import { convertToNxProjectGenerator } from '@nrwl/workspace';
import init from '../../generators/init/init';
import { postcssVersion } from '../../utils/versions';
import addLintingGenerator from '../add-linting/add-linting';
import karmaProjectGenerator from '../karma-project/karma-project';
import setupTailwindGenerator from '../setup-tailwind/setup-tailwind';
import { addModule } from './lib/add-module';
import { normalizeOptions } from './lib/normalize-options';
import { updateLibPackageNpmScope } from './lib/update-lib-package-npm-scope';
import { updateProject } from './lib/update-project';
import { updateTsConfig } from './lib/update-tsconfig';
import {
enableStrictTypeChecking,
setLibraryStrictDefault,
} from './lib/enable-strict-type-checking';
import { normalizeOptions } from './lib/normalize-options';
import { NormalizedSchema } from './lib/normalized-schema';
import { updateLibPackageNpmScope } from './lib/update-lib-package-npm-scope';
import { updateProject } from './lib/update-project';
import { updateTsConfig } from './lib/update-tsconfig';
import { Schema } from './schema';
import { convertToNxProjectGenerator } from '@nrwl/workspace';
export async function libraryGenerator(host: Tree, schema: Partial<Schema>) {
// Do some validation checks
if (!schema.routing && schema.lazy) {
throw new Error(`To use --lazy option, --routing must also be set.`);
throw new Error(`To use "--lazy" option, "--routing" must also be set.`);
}
if (schema.publishable === true && !schema.importPath) {
@ -38,6 +38,12 @@ export async function libraryGenerator(host: Tree, schema: Partial<Schema>) {
);
}
if (schema.addTailwind && !schema.buildable && !schema.publishable) {
throw new Error(
`To use "--addTailwind" option, you have to set either "--buildable" or "--publishable".`
);
}
const options = normalizeOptions(host, schema);
await init(host, {
@ -66,12 +72,19 @@ export async function libraryGenerator(host: Tree, schema: Partial<Schema>) {
setStrictMode(host, options);
await addLinting(host, options);
if (options.addTailwind) {
await setupTailwindGenerator(host, {
project: options.name,
skipFormat: true,
});
}
if (options.buildable || options.publishable) {
addDependenciesToPackageJson(
host,
{},
{
postcss: '^8.3.9',
postcss: postcssVersion,
'postcss-import': '^14.0.2',
'postcss-preset-env': '^6.7.0',
'postcss-url': '^10.1.1',

View File

@ -1,31 +1,31 @@
import { UnitTestRunner } from '../../utils/test-runners';
import { Linter } from '@nrwl/linter';
type AngularLinter = Exclude<Linter, Linter.TsLint>;
export interface Schema {
name: string;
skipFormat: boolean;
simpleModuleName: boolean;
addTailwind?: boolean;
skipFormat?: boolean;
simpleModuleName?: boolean;
addModuleSpec?: boolean;
directory?: string;
sourceDir?: string;
buildable: boolean;
publishable: boolean;
buildable?: boolean;
publishable?: boolean;
importPath?: string;
standaloneConfig?: boolean;
spec?: boolean;
flat?: boolean;
commonModule?: boolean;
prefix?: string;
routing?: boolean;
lazy?: boolean;
parentModule?: string;
tags?: string;
strict?: boolean;
linter: Exclude<Linter, Linter.TsLint>;
unitTestRunner: UnitTestRunner;
linter?: AngularLinter;
unitTestRunner?: UnitTestRunner;
compilationMode?: 'full' | 'partial';
setParserOptionsProject?: boolean;
}

View File

@ -112,7 +112,13 @@
"type": "boolean",
"description": "Whether or not to configure the ESLint \"parserOptions.project\" option. We do not do this by default for lint performance reasons.",
"default": false
},
"addTailwind": {
"type": "boolean",
"description": "Whether to configure TailwindCSS for the application. It can only be used with buildable and publishable libraries. Non-buildable libraries will use the application's Tailwind configuration.",
"default": false
}
},
"required": []
"additionalProperties": false,
"required": ["name"]
}

View File

@ -0,0 +1,18 @@
<% if (projectType === 'application') { %>const { createGlobPatternsForDependencies } = require('@nrwl/angular/tailwind');
const { join } = require('path');
module.exports = {
mode: 'jit',
purge: [
join(__dirname, '<%= relativeSourceRoot %>/**/*.{html,ts}'),
...createGlobPatternsForDependencies(__dirname),
],<% } else { %>module.exports = {<% } %>
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
};

View File

@ -0,0 +1,16 @@
<% if (projectType === 'application') { %>const { createGlobPatternsForDependencies } = require('@nrwl/angular/tailwind');
const { join } = require('path');
module.exports = {
content: [
join(__dirname, '<%= relativeSourceRoot %>/**/*.{html,ts}'),
...createGlobPatternsForDependencies(__dirname),
],<% } else { %>module.exports = {<% } %>
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
};

View File

@ -0,0 +1,56 @@
import {
joinPathFragments,
ProjectConfiguration,
stripIndents,
Tree,
updateProjectConfiguration,
} from '@nrwl/devkit';
import { NormalizedGeneratorOptions } from '../schema';
export function addTailwindConfigPathToProject(
tree: Tree,
options: NormalizedGeneratorOptions,
project: ProjectConfiguration
): void {
const buildTarget = project.targets?.[options.buildTarget];
if (!buildTarget) {
throw new Error(
stripIndents`The target "${options.buildTarget}" was not found for project "${options.project}".
If you are using a different build target, please provide it using the "--buildTarget" option.
If the project is not a buildable or publishable library, you don't need to setup TailwindCSS for it.`
);
}
const supportedLibraryExecutors = [
'@nrwl/angular:ng-packagr-lite',
'@nrwl/angular:package',
];
if (!supportedLibraryExecutors.includes(buildTarget.executor)) {
throw new Error(
stripIndents`The build target for project "${
options.project
}" is using an unsupported executor "${buildTarget.executor}".
Supported executors are ${supportedLibraryExecutors
.map((e) => `"${e}"`)
.join(', ')}.`
);
}
if (
buildTarget.options?.tailwindConfig &&
tree.exists(buildTarget.options.tailwindConfig)
) {
throw new Error(
stripIndents`The "${buildTarget.options.tailwindConfig}" file is already configured for the project "${options.project}". Are you sure this is the right project to set up Tailwind?
If you are sure, you can remove the configuration and re-run the generator.`
);
}
buildTarget.options = {
...buildTarget.options,
tailwindConfig: joinPathFragments(project.root, 'tailwind.config.js'),
};
updateProjectConfiguration(tree, options.project, project);
}

View File

@ -0,0 +1,36 @@
import {
generateFiles,
joinPathFragments,
ProjectConfiguration,
stripIndents,
Tree,
} from '@nrwl/devkit';
import { relative } from 'path';
import { GeneratorOptions } from '../schema';
export function addTailwindConfig(
tree: Tree,
options: GeneratorOptions,
project: ProjectConfiguration,
tailwindVersion: '2' | '3'
): void {
if (tree.exists(joinPathFragments(project.root, 'tailwind.config.js'))) {
throw new Error(
stripIndents`The "tailwind.config.js" file already exists in the project "${options.project}". Are you sure this is the right project to set up Tailwind?
If you are sure, you can remove the existing file and re-run the generator.`
);
}
const filesDir = tailwindVersion === '3' ? 'files/v3' : 'files/v2';
generateFiles(
tree,
joinPathFragments(__dirname, '..', filesDir),
project.root,
{
projectType: project.projectType,
relativeSourceRoot: relative(project.root, project.sourceRoot),
tmpl: '',
}
);
}

View File

@ -0,0 +1,22 @@
import {
addDependenciesToPackageJson,
GeneratorCallback,
Tree,
} from '@nrwl/devkit';
import {
autoprefixerVersion,
postcssVersion,
tailwindVersion,
} from '../../../utils/versions';
export function addTailwindRequiredPackages(tree: Tree): GeneratorCallback {
return addDependenciesToPackageJson(
tree,
{},
{
autoprefixer: autoprefixerVersion,
postcss: postcssVersion,
tailwindcss: tailwindVersion,
}
);
}

View File

@ -0,0 +1,24 @@
import { readJson, Tree } from '@nrwl/devkit';
import { checkAndCleanWithSemver } from '@nrwl/workspace';
import { lt } from 'semver';
export function detectTailwindInstalledVersion(
tree: Tree
): '2' | '3' | undefined {
const { dependencies, devDependencies } = readJson(tree, 'package.json');
const tailwindVersion =
dependencies?.tailwindcss ?? devDependencies?.tailwindcss;
if (!tailwindVersion) {
return undefined;
}
const version = checkAndCleanWithSemver('tailwindcss', tailwindVersion);
if (lt(version, '2.0.0')) {
throw new Error(
`The Tailwind CSS version "${tailwindVersion}" is not supported. Please upgrade to v2.0.0 or higher.`
);
}
return lt(version, '3.0.0') ? '2' : '3';
}

View File

@ -0,0 +1,6 @@
export * from './add-tailwind-config-path-to-project';
export * from './add-tailwind-config';
export * from './add-tailwind-required-packages';
export * from './detect-tailwind-installed-version';
export * from './normalize-options';
export * from './update-application-styles';

View File

@ -0,0 +1,10 @@
import type { GeneratorOptions, NormalizedGeneratorOptions } from '../schema';
export function normalizeOptions(
options: GeneratorOptions
): NormalizedGeneratorOptions {
return {
...options,
buildTarget: options.buildTarget || 'build',
};
}

View File

@ -0,0 +1,84 @@
import {
joinPathFragments,
ProjectConfiguration,
stripIndents,
Tree,
} from '@nrwl/devkit';
import { NormalizedGeneratorOptions } from '../schema';
export function updateApplicationStyles(
tree: Tree,
options: NormalizedGeneratorOptions,
project: ProjectConfiguration
): void {
let stylesEntryPoint = options.stylesEntryPoint;
if (stylesEntryPoint && !tree.exists(stylesEntryPoint)) {
throw new Error(
`The provided styles entry point "${stylesEntryPoint}" could not be found.`
);
}
if (!stylesEntryPoint) {
stylesEntryPoint = findStylesEntryPoint(tree, options, project);
if (!stylesEntryPoint) {
throw new Error(
stripIndents`Could not find a styles entry point for project "${options.project}".
Please specify a styles entry point using the "--stylesEntryPoint" option.`
);
}
}
const stylesEntryPointContent = tree.read(stylesEntryPoint, 'utf-8');
tree.write(
stylesEntryPoint,
stripIndents`@tailwind base;
@tailwind components;
@tailwind utilities;
${stylesEntryPointContent}`
);
}
function findStylesEntryPoint(
tree: Tree,
options: NormalizedGeneratorOptions,
project: ProjectConfiguration
): string | undefined {
// first check for common names
const possibleStylesEntryPoints = [
joinPathFragments(project.sourceRoot ?? project.root, 'styles.css'),
joinPathFragments(project.sourceRoot ?? project.root, 'styles.scss'),
joinPathFragments(project.sourceRoot ?? project.root, 'styles.sass'),
joinPathFragments(project.sourceRoot ?? project.root, 'styles.less'),
];
let stylesEntryPoint = possibleStylesEntryPoints.find((s) => tree.exists(s));
if (stylesEntryPoint) {
return stylesEntryPoint;
}
// then check for the specified styles in the build configuration if it exists
const styles: Array<string | { input: string; inject: boolean }> =
project.targets?.[options.buildTarget].options?.styles;
if (!styles) {
return undefined;
}
// find the first style that belongs to the project source
const style = styles.find((s) =>
typeof s === 'string'
? s.startsWith(project.root) && tree.exists(s)
: s.input.startsWith(project.root) &&
s.inject !== false &&
tree.exists(s.input)
);
if (!style) {
return undefined;
}
return typeof style === 'string' ? style : style.input;
}

View File

@ -0,0 +1,10 @@
export interface GeneratorOptions {
project: string;
buildTarget?: string;
skipFormat?: boolean;
stylesEntryPoint?: string;
}
export interface NormalizedGeneratorOptions extends GeneratorOptions {
buildTarget: string;
}

View File

@ -0,0 +1,34 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "NxAngularTailwindSetupGenerator",
"cli": "nx",
"title": "Configures TailwindCSS for an application or a buildable/publishable library.",
"description": "Adds the TailwindCSS configuration files for a given Angular project and installs, if needed, the packages required for TailwindCSS to work.",
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "The name of the project to add the TailwindCSS setup for.",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What project would you like to add the TailwindCSS setup?"
},
"buildTarget": {
"type": "string",
"description": "The name of the target used to build the project. This option only applies to buildable/publishable libraries.",
"default": "build"
},
"skipFormat": {
"type": "boolean",
"description": "Skips formatting the workspace after the generator completes."
},
"stylesEntryPoint": {
"type": "string",
"description": "Path to the styles entry point relative to the workspace root. If not provided the generator will do its best to find it and it will error if it can't. This option only applies to applications."
}
},
"additionalProperties": false,
"required": ["project"]
}

View File

@ -0,0 +1,441 @@
import {
addProjectConfiguration,
readJson,
readProjectConfiguration,
Tree,
updateProjectConfiguration,
} from '@nrwl/devkit';
import * as devkit from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { setupTailwindGenerator } from './setup-tailwind';
import {
autoprefixerVersion,
postcssVersion,
tailwindVersion,
} from '../../utils/versions';
describe('setupTailwind generator', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace(2);
jest.clearAllMocks();
});
it('should fail when the project does not exist', async () => {
await expect(
setupTailwindGenerator(tree, { project: 'not-found' })
).rejects.toThrow();
});
describe('application', () => {
const project = 'app1';
beforeEach(() => {
addProjectConfiguration(tree, project, {
name: project,
projectType: 'application',
root: `apps/${project}`,
sourceRoot: `apps/${project}/src`,
});
});
it('should throw when tailwind is installed as a dependency with a version lower than 2.0.0', async () => {
tree.write(
'package.json',
JSON.stringify({ dependencies: { tailwindcss: '^1.99.99' } })
);
await expect(setupTailwindGenerator(tree, { project })).rejects.toThrow(
`Tailwind CSS version "^1.99.99" is not supported. Please upgrade to v2.0.0 or higher.`
);
});
it('should throw when tailwind is installed as a devDependency with a version lower than 2.0.0', async () => {
tree.write(
'package.json',
JSON.stringify({ devDependencies: { tailwindcss: '^1.99.99' } })
);
await expect(setupTailwindGenerator(tree, { project })).rejects.toThrow(
`Tailwind CSS version "^1.99.99" is not supported. Please upgrade to v2.0.0 or higher.`
);
});
it('should throw when there is a tailwind.config.js file in the project', async () => {
tree.write(`apps/${project}/tailwind.config.js`, '');
await expect(setupTailwindGenerator(tree, { project })).rejects.toThrow(
expect.objectContaining({
message: expect.stringContaining(
`The "tailwind.config.js" file already exists in the project "${project}". Are you sure this is the right project to set up Tailwind?`
),
})
);
});
it('should throw when the provided styles entry point is not found', async () => {
const stylesEntryPoint = `apps/${project}/src/foo.scss`;
await expect(
setupTailwindGenerator(tree, { project, stylesEntryPoint })
).rejects.toThrow(
`The provided styles entry point "${stylesEntryPoint}" could not be found.`
);
});
it('should throw when the styles entry point is not provided and it is not found', async () => {
await expect(setupTailwindGenerator(tree, { project })).rejects.toThrow(
expect.objectContaining({
message: expect.stringContaining(
`Could not find a styles entry point for project "${project}"`
),
})
);
});
it('should throw when styles is not configured in the build config', async () => {
const projectConfig = readProjectConfiguration(tree, project);
projectConfig.targets = {
build: {
executor: '@nrwl/angular:webpack-browser',
options: {},
},
};
await expect(setupTailwindGenerator(tree, { project })).rejects.toThrow(
expect.objectContaining({
message: expect.stringContaining(
`Could not find a styles entry point for project "${project}"`
),
})
);
});
it('should throw when the styles configured in the build config do not exist', async () => {
const stylesEntryPoint = `apps/${project}/src/custom-styles-entry-point.scss`;
const projectConfig = readProjectConfiguration(tree, project);
projectConfig.targets = {
build: {
executor: '@nrwl/angular:webpack-browser',
options: {
styles: ['node_modules/awesome-ds/styles.css', stylesEntryPoint],
},
},
};
await expect(setupTailwindGenerator(tree, { project })).rejects.toThrow(
expect.objectContaining({
message: expect.stringContaining(
`Could not find a styles entry point for project "${project}"`
),
})
);
});
it('should throw when no styles within the project root are configured in the build config', async () => {
const projectConfig = readProjectConfiguration(tree, project);
projectConfig.targets = {
build: {
executor: '@nrwl/angular:webpack-browser',
options: {
styles: ['node_modules/awesome-ds/styles.css'],
},
},
};
await expect(setupTailwindGenerator(tree, { project })).rejects.toThrow(
expect.objectContaining({
message: expect.stringContaining(
`Could not find a styles entry point for project "${project}"`
),
})
);
});
it('should throw when the style inside the project root specified in the build config as an object has "inject: false"', async () => {
const stylesEntryPoint = `apps/${project}/src/custom-styles-entry-point.scss`;
tree.write(stylesEntryPoint, 'p { margin: 0; }');
const projectConfig = readProjectConfiguration(tree, project);
projectConfig.targets = {
build: {
executor: '@nrwl/angular:webpack-browser',
options: {
styles: [
'node_modules/awesome-ds/styles.css',
{
bundleName: 'styles.css',
input: stylesEntryPoint,
inject: false,
},
],
},
},
};
updateProjectConfiguration(tree, project, projectConfig);
await expect(setupTailwindGenerator(tree, { project })).rejects.toThrow(
expect.objectContaining({
message: expect.stringContaining(
`Could not find a styles entry point for project "${project}"`
),
})
);
});
it('should add tailwind styles to provided styles entry point', async () => {
const stylesEntryPoint = `apps/${project}/src/custom-styles-entry-point.scss`;
tree.write(stylesEntryPoint, 'p { margin: 0; }');
await setupTailwindGenerator(tree, { project, stylesEntryPoint });
expect(tree.read(stylesEntryPoint, 'utf-8')).toMatchInlineSnapshot(`
"@tailwind base;
@tailwind components;
@tailwind utilities;
p { margin: 0; }"
`);
});
it.each([
`apps/${project}/src/styles.css`,
`apps/${project}/src/styles.scss`,
`apps/${project}/src/styles.sass`,
`apps/${project}/src/styles.less`,
])(
'should add tailwind styles to "%s" when not provided',
async (stylesEntryPoint) => {
tree.write(stylesEntryPoint, 'p { margin: 0; }');
await setupTailwindGenerator(tree, { project });
expect(tree.read(stylesEntryPoint, 'utf-8')).toMatchInlineSnapshot(`
"@tailwind base;
@tailwind components;
@tailwind utilities;
p { margin: 0; }"
`);
}
);
it('should add tailwind styles to the first style inside the project root specified in the build config as a string', async () => {
const stylesEntryPoint = `apps/${project}/src/custom-styles-entry-point.scss`;
tree.write(stylesEntryPoint, 'p { margin: 0; }');
const projectConfig = readProjectConfiguration(tree, project);
projectConfig.targets = {
build: {
executor: '@nrwl/angular:webpack-browser',
options: {
styles: ['node_modules/awesome-ds/styles.css', stylesEntryPoint],
},
},
};
updateProjectConfiguration(tree, project, projectConfig);
await setupTailwindGenerator(tree, { project });
expect(tree.read(stylesEntryPoint, 'utf-8')).toMatchInlineSnapshot(`
"@tailwind base;
@tailwind components;
@tailwind utilities;
p { margin: 0; }"
`);
});
it('should add tailwind styles to the first style inside the project root specified in the build config as an object when inject is not specified', async () => {
const stylesEntryPoint = `apps/${project}/src/custom-styles-entry-point.scss`;
tree.write(stylesEntryPoint, 'p { margin: 0; }');
const projectConfig = readProjectConfiguration(tree, project);
projectConfig.targets = {
build: {
executor: '@nrwl/angular:webpack-browser',
options: {
styles: [
'node_modules/awesome-ds/styles.css',
{
bundleName: 'styles.css',
input: stylesEntryPoint,
},
],
},
},
};
updateProjectConfiguration(tree, project, projectConfig);
await setupTailwindGenerator(tree, { project });
expect(tree.read(stylesEntryPoint, 'utf-8')).toMatchInlineSnapshot(`
"@tailwind base;
@tailwind components;
@tailwind utilities;
p { margin: 0; }"
`);
});
it('should add tailwind styles to the first style inside the project root specified in the build config as an object when "inject: true"', async () => {
const stylesEntryPoint = `apps/${project}/src/custom-styles-entry-point.scss`;
tree.write(stylesEntryPoint, 'p { margin: 0; }');
const projectConfig = readProjectConfiguration(tree, project);
projectConfig.targets = {
build: {
executor: '@nrwl/angular:webpack-browser',
options: {
styles: [
'node_modules/awesome-ds/styles.css',
{
bundleName: 'styles.css',
input: stylesEntryPoint,
inject: true,
},
],
},
},
};
updateProjectConfiguration(tree, project, projectConfig);
await setupTailwindGenerator(tree, { project });
expect(tree.read(stylesEntryPoint, 'utf-8')).toMatchInlineSnapshot(`
"@tailwind base;
@tailwind components;
@tailwind utilities;
p { margin: 0; }"
`);
});
it('should add required packages', async () => {
const stylesEntryPoint = `apps/${project}/src/styles.scss`;
tree.write(stylesEntryPoint, 'p { margin: 0; }');
await setupTailwindGenerator(tree, { project, stylesEntryPoint });
const { devDependencies } = readJson(tree, 'package.json');
expect(devDependencies.tailwindcss).toBe(tailwindVersion);
expect(devDependencies.autoprefixer).toBe(autoprefixerVersion);
expect(devDependencies.postcss).toBe(postcssVersion);
});
it('should generate the tailwind.config.js file in the project root with the config for v3 by default', async () => {
const stylesEntryPoint = `apps/${project}/src/styles.scss`;
tree.write(stylesEntryPoint, 'p { margin: 0; }');
await setupTailwindGenerator(tree, { project, stylesEntryPoint });
expect(tree.read(`apps/${project}/tailwind.config.js`, 'utf-8'))
.toMatchInlineSnapshot(`
"const { createGlobPatternsForDependencies } = require('@nrwl/angular/tailwind');
const { join } = require('path');
module.exports = {
content: [
join(__dirname, 'src/**/*.{html,ts}'),
...createGlobPatternsForDependencies(__dirname),
],
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
};
"
`);
});
it('should generate the tailwind.config.js file in the project root with the config for v3 when a version greater than 3 is installed', async () => {
const stylesEntryPoint = `apps/${project}/src/styles.scss`;
tree.write(stylesEntryPoint, 'p { margin: 0; }');
tree.write(
'package.json',
JSON.stringify({ devDependencies: { tailwindcss: '^3.0.1' } })
);
await setupTailwindGenerator(tree, { project, stylesEntryPoint });
expect(tree.read(`apps/${project}/tailwind.config.js`, 'utf-8'))
.toMatchInlineSnapshot(`
"const { createGlobPatternsForDependencies } = require('@nrwl/angular/tailwind');
const { join } = require('path');
module.exports = {
content: [
join(__dirname, 'src/**/*.{html,ts}'),
...createGlobPatternsForDependencies(__dirname),
],
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
};
"
`);
});
it('should generate the tailwind.config.js file in the project root with the config for v2 when a version greater than 2 and lower than 3 is installed', async () => {
const stylesEntryPoint = `apps/${project}/src/styles.scss`;
tree.write(stylesEntryPoint, 'p { margin: 0; }');
tree.write(
'package.json',
JSON.stringify({ devDependencies: { tailwindcss: '~2.0.0' } })
);
await setupTailwindGenerator(tree, { project, stylesEntryPoint });
expect(tree.read(`apps/${project}/tailwind.config.js`, 'utf-8'))
.toMatchInlineSnapshot(`
"const { createGlobPatternsForDependencies } = require('@nrwl/angular/tailwind');
const { join } = require('path');
module.exports = {
mode: 'jit',
purge: [
join(__dirname, 'src/**/*.{html,ts}'),
...createGlobPatternsForDependencies(__dirname),
],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
};
"
`);
});
it('should format files', async () => {
const stylesEntryPoint = `apps/${project}/src/styles.scss`;
tree.write(stylesEntryPoint, 'p { margin: 0; }');
jest.spyOn(devkit, 'formatFiles');
await setupTailwindGenerator(tree, { project, stylesEntryPoint });
expect(devkit.formatFiles).toHaveBeenCalled();
});
it('should not format files when "skipFormat: true"', async () => {
const stylesEntryPoint = `apps/${project}/src/styles.scss`;
tree.write(stylesEntryPoint, 'p { margin: 0; }');
jest.spyOn(devkit, 'formatFiles');
await setupTailwindGenerator(tree, {
project,
stylesEntryPoint,
skipFormat: true,
});
expect(devkit.formatFiles).not.toHaveBeenCalled();
});
});
});

View File

@ -0,0 +1,4 @@
import { convertNxGenerator } from '@nrwl/devkit';
import { setupTailwindGenerator } from './setup-tailwind';
export default convertNxGenerator(setupTailwindGenerator);

View File

@ -0,0 +1,296 @@
import {
addProjectConfiguration,
readJson,
readProjectConfiguration,
Tree,
updateProjectConfiguration,
} from '@nrwl/devkit';
import * as devkit from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { setupTailwindGenerator } from './setup-tailwind';
import {
autoprefixerVersion,
postcssVersion,
tailwindVersion,
} from '../../utils/versions';
describe('setupTailwind generator', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace(2);
jest.clearAllMocks();
});
it('should fail when the project does not exist', async () => {
await expect(
setupTailwindGenerator(tree, { project: 'not-found' })
).rejects.toThrow();
});
describe('libraries', () => {
const project = 'lib1';
beforeEach(() => {
addProjectConfiguration(tree, project, {
name: project,
projectType: 'library',
root: `libs/${project}`,
sourceRoot: `libs/${project}/src`,
});
});
it('should throw when tailwind is installed as a dependency with a version lower than 2.0.0', async () => {
tree.write(
'package.json',
JSON.stringify({ dependencies: { tailwindcss: '^1.99.99' } })
);
await expect(setupTailwindGenerator(tree, { project })).rejects.toThrow(
`Tailwind CSS version "^1.99.99" is not supported. Please upgrade to v2.0.0 or higher.`
);
});
it('should throw when tailwind is installed as a devDependency with a version lower than 2.0.0', async () => {
tree.write(
'package.json',
JSON.stringify({ devDependencies: { tailwindcss: '^1.99.99' } })
);
await expect(setupTailwindGenerator(tree, { project })).rejects.toThrow(
`Tailwind CSS version "^1.99.99" is not supported. Please upgrade to v2.0.0 or higher.`
);
});
it('should throw when the build target is not found', async () => {
await expect(setupTailwindGenerator(tree, { project })).rejects.toThrow(
expect.objectContaining({
message: expect.stringContaining(
`The target "build" was not found for project "${project}".`
),
})
);
});
it('should throw when the specified build target is not found', async () => {
await expect(
setupTailwindGenerator(tree, { project, buildTarget: 'custom-build' })
).rejects.toThrow(
expect.objectContaining({
message: expect.stringContaining(
`The target "custom-build" was not found for project "${project}".`
),
})
);
});
it('should throw when the build target is using an unsupported executor', async () => {
const projectConfig = readProjectConfiguration(tree, project);
projectConfig.targets = {
build: {
executor: '@angular/build-angular:browser',
options: {},
},
};
updateProjectConfiguration(tree, project, projectConfig);
await expect(setupTailwindGenerator(tree, { project })).rejects.toThrow(
expect.objectContaining({
message: expect.stringContaining(
`The build target for project "${project}" is using an unsupported executor "@angular/build-angular:browser".`
),
})
);
});
it('should throw when the tailwind config is configured in the build target and the file it points to exists', async () => {
const tailwindConfig = `libs/${project}/my-tailwind.config.js`;
let projectConfig = readProjectConfiguration(tree, project);
projectConfig.targets = {
build: {
executor: '@nrwl/angular:package',
options: { tailwindConfig },
},
};
updateProjectConfiguration(tree, project, projectConfig);
tree.write(tailwindConfig, '');
await expect(setupTailwindGenerator(tree, { project })).rejects.toThrow(
expect.objectContaining({
message: expect.stringContaining(
`The "${tailwindConfig}" file is already configured for the project "${project}". Are you sure this is the right project to set up Tailwind?`
),
})
);
});
it('should add the tailwind config path to the "build" target by default when no build target is specified', async () => {
let projectConfig = readProjectConfiguration(tree, project);
projectConfig.targets = {
build: { executor: '@nrwl/angular:package', options: {} },
};
updateProjectConfiguration(tree, project, projectConfig);
await setupTailwindGenerator(tree, { project });
projectConfig = readProjectConfiguration(tree, project);
expect(projectConfig.targets.build.options.tailwindConfig).toBe(
`libs/${project}/tailwind.config.js`
);
});
it('should add the tailwind config path to the specified buildTarget', async () => {
const buildTarget = 'custom-build';
let projectConfig = readProjectConfiguration(tree, project);
projectConfig.targets = {
[buildTarget]: { executor: '@nrwl/angular:package', options: {} },
};
updateProjectConfiguration(tree, project, projectConfig);
await setupTailwindGenerator(tree, { project, buildTarget });
projectConfig = readProjectConfiguration(tree, project);
expect(projectConfig.targets[buildTarget].options.tailwindConfig).toBe(
`libs/${project}/tailwind.config.js`
);
});
it.each(['@nrwl/angular:ng-packagr-lite', '@nrwl/angular:package'])(
'should add the tailwind config path when using the "%s" executor',
async (executor) => {
let projectConfig = readProjectConfiguration(tree, project);
projectConfig.targets = { build: { executor, options: {} } };
updateProjectConfiguration(tree, project, projectConfig);
await setupTailwindGenerator(tree, { project });
projectConfig = readProjectConfiguration(tree, project);
expect(projectConfig.targets.build.options.tailwindConfig).toBe(
`libs/${project}/tailwind.config.js`
);
}
);
it('should add required packages', async () => {
const projectConfig = readProjectConfiguration(tree, project);
projectConfig.targets = {
build: { executor: '@nrwl/angular:package', options: {} },
};
updateProjectConfiguration(tree, project, projectConfig);
await setupTailwindGenerator(tree, { project });
const { devDependencies } = readJson(tree, 'package.json');
expect(devDependencies.tailwindcss).toBe(tailwindVersion);
expect(devDependencies.autoprefixer).toBe(autoprefixerVersion);
expect(devDependencies.postcss).toBe(postcssVersion);
});
it('should generate the tailwind.config.js file in the project root for v3 by default', async () => {
const projectConfig = readProjectConfiguration(tree, project);
projectConfig.targets = {
build: { executor: '@nrwl/angular:package', options: {} },
};
updateProjectConfiguration(tree, project, projectConfig);
await setupTailwindGenerator(tree, { project });
expect(tree.read(`libs/${project}/tailwind.config.js`, 'utf-8'))
.toMatchInlineSnapshot(`
"module.exports = {
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
};
"
`);
});
it('should generate the tailwind.config.js file in the project root with the config for v3 when a version greater than 3 is installed', async () => {
const projectConfig = readProjectConfiguration(tree, project);
projectConfig.targets = {
build: { executor: '@nrwl/angular:package', options: {} },
};
updateProjectConfiguration(tree, project, projectConfig);
tree.write(
'package.json',
JSON.stringify({ devDependencies: { tailwindcss: '^3.0.1' } })
);
await setupTailwindGenerator(tree, { project });
expect(tree.read(`libs/${project}/tailwind.config.js`, 'utf-8'))
.toMatchInlineSnapshot(`
"module.exports = {
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
};
"
`);
});
it('should generate the tailwind.config.js file in the project root with the config for v2 when a version greater than 2 and lower than 3 is installed', async () => {
const projectConfig = readProjectConfiguration(tree, project);
projectConfig.targets = {
build: { executor: '@nrwl/angular:package', options: {} },
};
updateProjectConfiguration(tree, project, projectConfig);
tree.write(
'package.json',
JSON.stringify({ devDependencies: { tailwindcss: '~2.0.0' } })
);
await setupTailwindGenerator(tree, { project });
expect(tree.read(`libs/${project}/tailwind.config.js`, 'utf-8'))
.toMatchInlineSnapshot(`
"module.exports = {
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
};
"
`);
});
it('should format files', async () => {
const projectConfig = readProjectConfiguration(tree, project);
projectConfig.targets = {
build: { executor: '@nrwl/angular:package', options: {} },
};
updateProjectConfiguration(tree, project, projectConfig);
jest.spyOn(devkit, 'formatFiles');
await setupTailwindGenerator(tree, { project });
expect(devkit.formatFiles).toHaveBeenCalled();
});
it('should not format files when "skipFormat: true"', async () => {
const projectConfig = readProjectConfiguration(tree, project);
projectConfig.targets = {
build: { executor: '@nrwl/angular:package', options: {} },
};
updateProjectConfiguration(tree, project, projectConfig);
jest.spyOn(devkit, 'formatFiles');
await setupTailwindGenerator(tree, { project, skipFormat: true });
expect(devkit.formatFiles).not.toHaveBeenCalled();
});
});
});

View File

@ -0,0 +1,46 @@
import {
formatFiles,
GeneratorCallback,
readProjectConfiguration,
Tree,
} from '@nrwl/devkit';
import {
addTailwindConfig,
addTailwindConfigPathToProject,
addTailwindRequiredPackages,
detectTailwindInstalledVersion,
normalizeOptions,
updateApplicationStyles,
} from './lib';
import { GeneratorOptions } from './schema';
export async function setupTailwindGenerator(
tree: Tree,
rawOptions: GeneratorOptions
): Promise<GeneratorCallback | undefined> {
const options = normalizeOptions(rawOptions);
const project = readProjectConfiguration(tree, options.project);
const tailwindInstalledVersion = detectTailwindInstalledVersion(tree);
let installTask: GeneratorCallback | undefined;
if (tailwindInstalledVersion === undefined) {
installTask = addTailwindRequiredPackages(tree);
}
addTailwindConfig(tree, options, project, tailwindInstalledVersion ?? '3');
if (project.projectType === 'application') {
updateApplicationStyles(tree, options, project);
} else if (project.projectType === 'library') {
addTailwindConfigPathToProject(tree, options, project);
}
if (!options.skipFormat) {
await formatFiles(tree);
}
return installTask;
}
export default setupTailwindGenerator;

View File

@ -7,3 +7,6 @@ export const rxjsVersion = '~7.4.0';
export const jestPresetAngularVersion = '11.0.0';
export const angularEslintVersion = '~13.0.1';
export const angularArchitectsModuleFederationPluginVersion = '^13.0.1';
export const tailwindVersion = '^3.0.2';
export const postcssVersion = '^8.4.5';
export const autoprefixerVersion = '^10.4.0';