feat(angular): add support for configuring tailwind in existing and new apps and buildable/publishable libs (#8043)
This commit is contained in:
parent
45987c40d1
commit
2b00c99db1
@ -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`
|
||||
|
||||
@ -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`
|
||||
|
||||
56
docs/angular/api-angular/generators/setup-tailwind.md
Normal file
56
docs/angular/api-angular/generators/setup-tailwind.md
Normal 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.
|
||||
@ -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",
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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`
|
||||
|
||||
56
docs/node/api-angular/generators/setup-tailwind.md
Normal file
56
docs/node/api-angular/generators/setup-tailwind.md
Normal 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.
|
||||
@ -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`
|
||||
|
||||
@ -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`
|
||||
|
||||
56
docs/react/api-angular/generators/setup-tailwind.md
Normal file
56
docs/react/api-angular/generators/setup-tailwind.md
Normal 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.
|
||||
@ -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`));
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
398
e2e/angular-extensions/src/tailwind.test.ts
Normal file
398
e2e/angular-extensions/src/tailwind.test.ts
Normal 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}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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",
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -15,7 +15,8 @@
|
||||
"webpack-merge",
|
||||
"find-parent-dir",
|
||||
"ts-node",
|
||||
"tsconfig-paths"
|
||||
"tsconfig-paths",
|
||||
"semver"
|
||||
],
|
||||
"keepLifecycleScripts": true
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
|
||||
@ -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'] || {}),
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
|
||||
@ -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: [],
|
||||
};
|
||||
@ -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: [],
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
@ -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: '',
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -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';
|
||||
}
|
||||
@ -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';
|
||||
@ -0,0 +1,10 @@
|
||||
import type { GeneratorOptions, NormalizedGeneratorOptions } from '../schema';
|
||||
|
||||
export function normalizeOptions(
|
||||
options: GeneratorOptions
|
||||
): NormalizedGeneratorOptions {
|
||||
return {
|
||||
...options,
|
||||
buildTarget: options.buildTarget || 'build',
|
||||
};
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
10
packages/angular/src/generators/setup-tailwind/schema.d.ts
vendored
Normal file
10
packages/angular/src/generators/setup-tailwind/schema.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
export interface GeneratorOptions {
|
||||
project: string;
|
||||
buildTarget?: string;
|
||||
skipFormat?: boolean;
|
||||
stylesEntryPoint?: string;
|
||||
}
|
||||
|
||||
export interface NormalizedGeneratorOptions extends GeneratorOptions {
|
||||
buildTarget: string;
|
||||
}
|
||||
34
packages/angular/src/generators/setup-tailwind/schema.json
Normal file
34
packages/angular/src/generators/setup-tailwind/schema.json
Normal 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"]
|
||||
}
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,4 @@
|
||||
import { convertNxGenerator } from '@nrwl/devkit';
|
||||
import { setupTailwindGenerator } from './setup-tailwind';
|
||||
|
||||
export default convertNxGenerator(setupTailwindGenerator);
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
@ -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';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user