From 9a08a83591645bc2e42f21cd7390ddd0dae43eab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leosvel=20P=C3=A9rez=20Espinosa?= Date: Thu, 2 Dec 2021 11:16:01 +0000 Subject: [PATCH] feat(angular): add tailwind support for buildable libraries (#7961) --- .../api-angular/executors/ng-packagr-lite.md | 6 + docs/angular/api-angular/executors/package.md | 6 + .../api-angular/executors/ng-packagr-lite.md | 6 + docs/node/api-angular/executors/package.md | 6 + .../api-angular/executors/ng-packagr-lite.md | 6 + docs/react/api-angular/executors/package.md | 6 + .../src/angular-library.test.ts | 299 +++++++++++++-- .../ng-package/entry-point/compile-ngc.di.ts | 22 +- .../entry-point/compile-ngc.transform.ts | 15 +- .../entry-point/write-package.di.ts | 5 +- .../ng-package/options.di.ts | 47 +++ .../ng-package/package.di.ts | 11 +- .../styles/stylesheet-processor.di.ts | 18 + .../styles/stylesheet-processor.ts | 362 ++++++++++++++++++ .../ng-packagr-lite.impl.spec.ts | 18 +- .../ng-packagr-lite/ng-packagr-lite.impl.ts | 5 + .../src/executors/ng-packagr-lite/schema.json | 4 + .../ng-package/entry-point/compile-ngc.di.ts | 4 +- .../entry-point/compile-ngc.transform.ts | 5 +- .../ng-package/options.di.ts | 47 +++ .../ng-package/package.di.ts | 11 +- .../styles/stylesheet-processor.ts | 7 +- .../executors/package/package.impl.spec.ts | 18 +- .../src/executors/package/package.impl.ts | 11 +- .../angular/src/executors/package/schema.d.ts | 3 +- .../angular/src/executors/package/schema.json | 4 + .../utilities/tailwindcss.ts | 24 +- 27 files changed, 886 insertions(+), 90 deletions(-) create mode 100644 packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/options.di.ts create mode 100644 packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/styles/stylesheet-processor.di.ts create mode 100644 packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/styles/stylesheet-processor.ts create mode 100644 packages/angular/src/executors/package/ng-packagr-adjustments/ng-package/options.di.ts rename packages/angular/src/executors/{package/ng-packagr-adjustments => }/utilities/tailwindcss.ts (76%) diff --git a/docs/angular/api-angular/executors/ng-packagr-lite.md b/docs/angular/api-angular/executors/ng-packagr-lite.md index 46b4741641..1a910703c0 100644 --- a/docs/angular/api-angular/executors/ng-packagr-lite.md +++ b/docs/angular/api-angular/executors/ng-packagr-lite.md @@ -22,6 +22,12 @@ Possible values: `dependencies`, `peerDependencies` When `updateBuildableProjectDepsInPackageJson` is `true`, this adds dependencies to either `peerDependencies` or `dependencies`. +### tailwindConfig + +Type: `string` + +The full path for the Tailwind configuration file, relative to the workspace root. If not provided and a `tailwind.config.js` file exists in the project or workspace root, it will be used. Otherwise, Tailwind will not be configured. + ### tsConfig Type: `string` diff --git a/docs/angular/api-angular/executors/package.md b/docs/angular/api-angular/executors/package.md index 05d8c7ba7e..c177134d1b 100644 --- a/docs/angular/api-angular/executors/package.md +++ b/docs/angular/api-angular/executors/package.md @@ -22,6 +22,12 @@ Possible values: `dependencies`, `peerDependencies` When `updateBuildableProjectDepsInPackageJson` is `true`, this adds dependencies to either `peerDependencies` or `dependencies`. +### tailwindConfig + +Type: `string` + +The full path for the Tailwind configuration file, relative to the workspace root. If not provided and a `tailwind.config.js` file exists in the project or workspace root, it will be used. Otherwise, Tailwind will not be configured. + ### tsConfig Type: `string` diff --git a/docs/node/api-angular/executors/ng-packagr-lite.md b/docs/node/api-angular/executors/ng-packagr-lite.md index 5600146283..8a961af8ba 100644 --- a/docs/node/api-angular/executors/ng-packagr-lite.md +++ b/docs/node/api-angular/executors/ng-packagr-lite.md @@ -22,6 +22,12 @@ Possible values: `dependencies`, `peerDependencies` When `updateBuildableProjectDepsInPackageJson` is `true`, this adds dependencies to either `peerDependencies` or `dependencies`. +### tailwindConfig + +Type: `string` + +The full path for the Tailwind configuration file, relative to the workspace root. If not provided and a `tailwind.config.js` file exists in the project or workspace root, it will be used. Otherwise, Tailwind will not be configured. + ### tsConfig Type: `string` diff --git a/docs/node/api-angular/executors/package.md b/docs/node/api-angular/executors/package.md index f1f814f0b2..c0cb8912e7 100644 --- a/docs/node/api-angular/executors/package.md +++ b/docs/node/api-angular/executors/package.md @@ -22,6 +22,12 @@ Possible values: `dependencies`, `peerDependencies` When `updateBuildableProjectDepsInPackageJson` is `true`, this adds dependencies to either `peerDependencies` or `dependencies`. +### tailwindConfig + +Type: `string` + +The full path for the Tailwind configuration file, relative to the workspace root. If not provided and a `tailwind.config.js` file exists in the project or workspace root, it will be used. Otherwise, Tailwind will not be configured. + ### tsConfig Type: `string` diff --git a/docs/react/api-angular/executors/ng-packagr-lite.md b/docs/react/api-angular/executors/ng-packagr-lite.md index 5600146283..8a961af8ba 100644 --- a/docs/react/api-angular/executors/ng-packagr-lite.md +++ b/docs/react/api-angular/executors/ng-packagr-lite.md @@ -22,6 +22,12 @@ Possible values: `dependencies`, `peerDependencies` When `updateBuildableProjectDepsInPackageJson` is `true`, this adds dependencies to either `peerDependencies` or `dependencies`. +### tailwindConfig + +Type: `string` + +The full path for the Tailwind configuration file, relative to the workspace root. If not provided and a `tailwind.config.js` file exists in the project or workspace root, it will be used. Otherwise, Tailwind will not be configured. + ### tsConfig Type: `string` diff --git a/docs/react/api-angular/executors/package.md b/docs/react/api-angular/executors/package.md index f1f814f0b2..c0cb8912e7 100644 --- a/docs/react/api-angular/executors/package.md +++ b/docs/react/api-angular/executors/package.md @@ -22,6 +22,12 @@ Possible values: `dependencies`, `peerDependencies` When `updateBuildableProjectDepsInPackageJson` is `true`, this adds dependencies to either `peerDependencies` or `dependencies`. +### tailwindConfig + +Type: `string` + +The full path for the Tailwind configuration file, relative to the workspace root. If not provided and a `tailwind.config.js` file exists in the project or workspace root, it will be used. Otherwise, Tailwind will not be configured. + ### tsConfig Type: `string` diff --git a/e2e/angular-extensions/src/angular-library.test.ts b/e2e/angular-extensions/src/angular-library.test.ts index 3f75095d5e..01f183076d 100644 --- a/e2e/angular-extensions/src/angular-library.test.ts +++ b/e2e/angular-extensions/src/angular-library.test.ts @@ -4,6 +4,8 @@ import { checkFilesExist, getSelectedPackageManager, newProject, + packageInstall, + readFile, readJson, removeProject, runCLI, @@ -11,6 +13,7 @@ import { updateFile, } from '@nrwl/e2e/utils'; import { names } from '@nrwl/devkit'; +import { join } from 'path'; describe('Angular Package', () => { ['publishable', 'buildable'].forEach((testConfig) => { @@ -22,7 +25,6 @@ describe('Angular Package', () => { * / * parentLib => * \ - * \ * childLib2 * */ @@ -141,13 +143,7 @@ describe('Angular Package', () => { afterEach(() => removeProject({ onlyOnCI: true })); - it('should build the library when it does not have any deps', () => { - runCLI(`build ${childLib}`); - - checkFilesExist(`dist/libs/${childLib}/package.json`); - }); - - it('should properly add references to any dependency into the parent package.json', () => { + it('should build properly and update the parent package.json with the dependencies', () => { runCLI(`build ${childLib}`); runCLI(`build ${childLib2}`); runCLI(`build ${parentLib}`); @@ -173,56 +169,281 @@ describe('Angular Package', () => { describe('Publishable library secondary entry point', () => { let project: string; - let parentLib: string; - let childLib: string; + let lib: string; let entryPoint: string; beforeEach(() => { project = newProject(); - parentLib = uniq('parentlib'); - childLib = uniq('childlib'); + lib = uniq('lib'); entryPoint = uniq('entrypoint'); runCLI( - `generate @nrwl/angular:lib ${parentLib} --publishable --importPath=@${project}/${parentLib} --no-interactive` + `generate @nrwl/angular:lib ${lib} --publishable --importPath=@${project}/${lib} --no-interactive` ); runCLI( - `generate @nrwl/angular:lib ${childLib} --publishable --importPath=@${project}/${childLib} --no-interactive` - ); - runCLI( - `generate @nrwl/angular:secondary-entry-point --name=${entryPoint} --library=${childLib} --no-interactive` - ); - - updateFile( - `libs/${parentLib}/src/lib/${parentLib}.module.ts`, - ` - import { NgModule } from '@angular/core'; - import { CommonModule } from '@angular/common'; - import { ${ - names(entryPoint).className - }Module } from '@${project}/${childLib}/${entryPoint}'; - - @NgModule({ - imports: [CommonModule, ${names(entryPoint).className}Module], - }) - export class Lib1Module {} - ` + `generate @nrwl/angular:secondary-entry-point --name=${entryPoint} --library=${lib} --no-interactive` ); }); it('should build successfully', () => { - const buildOutput = runCLI(`build ${parentLib}`); + const buildOutput = runCLI(`build ${lib}`); expect(buildOutput).toContain( - `Building entry point '@${project}/${childLib}'` + `Building entry point '@${project}/${lib}'` ); expect(buildOutput).toContain( - `Building entry point '@${project}/${childLib}/${entryPoint}'` - ); - expect(buildOutput).toContain( - `Building entry point '@${project}/${parentLib}'` + `Building entry point '@${project}/${lib}/${entryPoint}'` ); 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: '', + 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); + }); + }); }); diff --git a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/entry-point/compile-ngc.di.ts b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/entry-point/compile-ngc.di.ts index dcba403baf..075f7ddf83 100644 --- a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/entry-point/compile-ngc.di.ts +++ b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/entry-point/compile-ngc.di.ts @@ -1,11 +1,21 @@ +/** + * Adapted from the original ng-packagr source. + * + * Changes made: + * - Use our own compileNgcTransformFactory instead of the one provided by ng-packagr. + * - Use NX_STYLESHEET_PROCESSOR instead of STYLESHEET_PROCESSOR. + * - Use NX_STYLESHEET_PROCESSOR_TOKEN instead of STYLESHEET_PROCESSOR_TOKEN. + * - USE NX_OPTIONS_TOKEN instead of OPTIONS_TOKEN. + */ + import { InjectionToken, Provider } from 'injection-js'; import type { Transform } from 'ng-packagr/lib/graph/transform'; import { provideTransform } from 'ng-packagr/lib/graph/transform.di'; -import { OPTIONS_TOKEN } from 'ng-packagr/lib/ng-package/options.di'; import { - STYLESHEET_PROCESSOR, - STYLESHEET_PROCESSOR_TOKEN, -} from 'ng-packagr/lib/styles/stylesheet-processor.di'; + NX_STYLESHEET_PROCESSOR, + NX_STYLESHEET_PROCESSOR_TOKEN, +} from '../../styles/stylesheet-processor.di'; +import { NX_OPTIONS_TOKEN } from '../options.di'; import { nxCompileNgcTransformFactory } from './compile-ngc.transform'; export const NX_COMPILE_NGC_TOKEN = new InjectionToken( @@ -14,9 +24,9 @@ export const NX_COMPILE_NGC_TOKEN = new InjectionToken( export const NX_COMPILE_NGC_TRANSFORM = provideTransform({ provide: NX_COMPILE_NGC_TOKEN, useFactory: nxCompileNgcTransformFactory, - deps: [STYLESHEET_PROCESSOR_TOKEN, OPTIONS_TOKEN], + deps: [NX_STYLESHEET_PROCESSOR_TOKEN, NX_OPTIONS_TOKEN], }); export const NX_COMPILE_NGC_PROVIDERS: Provider[] = [ - STYLESHEET_PROCESSOR, + NX_STYLESHEET_PROCESSOR, NX_COMPILE_NGC_TRANSFORM, ]; diff --git a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/entry-point/compile-ngc.transform.ts b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/entry-point/compile-ngc.transform.ts index e134bd3cdf..82dcb6c5da 100644 --- a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/entry-point/compile-ngc.transform.ts +++ b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/entry-point/compile-ngc.transform.ts @@ -1,8 +1,9 @@ /** * Adapted from the original ngPackagr source. * - * Excludes the ngcc compilation which is not needed - * since these libraries will be compiled by the ngtsc. + * Changes made: + * - Use our own StylesheetProcessor files instead of the ones provide by ng-packagr. + * - Excludes the ngcc compilation for faster builds. */ import type { Transform } from 'ng-packagr/lib/graph/transform'; @@ -11,12 +12,12 @@ import { isEntryPoint, isEntryPointInProgress, } from 'ng-packagr/lib/ng-package/nodes'; -import { NgPackagrOptions } from 'ng-packagr/lib/ng-package/options.di'; -import type { StylesheetProcessor as StylesheetProcessorClass } from 'ng-packagr/lib/styles/stylesheet-processor'; import { setDependenciesTsConfigPaths } from 'ng-packagr/lib/ts/tsconfig'; import * as path from 'path'; import * as ts from 'typescript'; import { compileSourceFiles } from '../../ngc/compile-source-files'; +import { StylesheetProcessor as StylesheetProcessorClass } from '../../styles/stylesheet-processor'; +import { NgPackagrOptions } from '../options.di'; export const nxCompileNgcTransformFactory = ( StylesheetProcessor: typeof StylesheetProcessorClass, @@ -42,8 +43,10 @@ export const nxCompileNgcTransformFactory = ( basePath, cssUrl, styleIncludePaths, - options.cacheEnabled && options.cacheDirectory - ); + options.cacheEnabled && options.cacheDirectory, + options.watch, + options.tailwindConfig + ) as any; await compileSourceFiles( graph, diff --git a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/entry-point/write-package.di.ts b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/entry-point/write-package.di.ts index 92f914f0ac..b1038c6931 100644 --- a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/entry-point/write-package.di.ts +++ b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/entry-point/write-package.di.ts @@ -3,6 +3,7 @@ * * Changes made: * - Provide our own writePackageTransform function. + * - USE NX_OPTIONS_TOKEN instead of OPTIONS_TOKEN. */ import { InjectionToken } from 'injection-js'; @@ -11,7 +12,7 @@ import { provideTransform, TransformProvider, } from 'ng-packagr/lib/graph/transform.di'; -import { OPTIONS_TOKEN } from 'ng-packagr/lib/ng-package/options.di'; +import { NX_OPTIONS_TOKEN } from '../options.di'; import { nxWritePackageTransform } from './write-package.transform'; export const NX_WRITE_PACKAGE_TRANSFORM_TOKEN = new InjectionToken( @@ -20,5 +21,5 @@ export const NX_WRITE_PACKAGE_TRANSFORM_TOKEN = new InjectionToken( export const NX_WRITE_PACKAGE_TRANSFORM: TransformProvider = provideTransform({ provide: NX_WRITE_PACKAGE_TRANSFORM_TOKEN, useFactory: nxWritePackageTransform, - deps: [OPTIONS_TOKEN], + deps: [NX_OPTIONS_TOKEN], }); diff --git a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/options.di.ts b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/options.di.ts new file mode 100644 index 0000000000..bcaaeba992 --- /dev/null +++ b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/options.di.ts @@ -0,0 +1,47 @@ +/** + * Adapted from the original ng-packagr. + * + * Changes made: + * - Use our own options interface to add support for tailwindConfig. + */ + +import * as findCacheDirectory from 'find-cache-dir'; +import { InjectionToken, Provider, ValueProvider } from 'injection-js'; +import { NgPackagrOptions as NgPackagrOptionsBase } from 'ng-packagr/lib/ng-package/options.di'; +import { tmpdir } from 'os'; +import { resolve } from 'path'; + +export interface NgPackagrOptions extends NgPackagrOptionsBase { + tailwindConfig?: string; +} + +export const NX_OPTIONS_TOKEN = new InjectionToken( + `nx.v1.options` +); + +export const nxProvideOptions = ( + options: NgPackagrOptions = {} +): ValueProvider => ({ + provide: NX_OPTIONS_TOKEN, + useValue: normalizeOptions(options), +}); + +export const NX_DEFAULT_OPTIONS_PROVIDER: Provider = nxProvideOptions(); + +function normalizeOptions(options: NgPackagrOptions = {}) { + const ciEnv = process.env['CI']; + const isCI = ciEnv?.toLowerCase() === 'true' || ciEnv === '1'; + const { cacheEnabled = !isCI, cacheDirectory = findCachePath() } = options; + + return { + ...options, + cacheEnabled, + cacheDirectory, + }; +} + +function findCachePath(): string { + const name = 'ng-packagr'; + + return findCacheDirectory({ name }) || resolve(tmpdir(), name); +} diff --git a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/package.di.ts b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/package.di.ts index 117e0a0daf..53c385333e 100644 --- a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/package.di.ts +++ b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/package.di.ts @@ -3,6 +3,8 @@ * * Changes made: * - Use NX_ENTRY_POINT_TRANSFORM_TOKEN instead of ENTRY_POINT_TRANSFORM_TOKEN. + * - USE NX_OPTIONS_TOKEN instead of OPTIONS_TOKEN. + * - USE NX_DEFAULT_OPTIONS_PROVIDER instead of DEFAULT_OPTIONS_PROVIDER. */ import type { Provider } from 'injection-js'; @@ -17,13 +19,10 @@ import { INIT_TS_CONFIG_TOKEN, INIT_TS_CONFIG_TRANSFORM, } from 'ng-packagr/lib/ng-package/entry-point/init-tsconfig.di'; -import { - DEFAULT_OPTIONS_PROVIDER, - OPTIONS_TOKEN, -} from 'ng-packagr/lib/ng-package/options.di'; import { packageTransformFactory } from 'ng-packagr/lib/ng-package/package.transform'; import { PROJECT_TOKEN } from 'ng-packagr/lib/project.di'; import { NX_ENTRY_POINT_TRANSFORM_TOKEN } from './entry-point/entry-point.di'; +import { NX_DEFAULT_OPTIONS_PROVIDER, NX_OPTIONS_TOKEN } from './options.di'; export const NX_PACKAGE_TRANSFORM_TOKEN = new InjectionToken( `nx.v1.packageTransform` @@ -34,7 +33,7 @@ export const NX_PACKAGE_TRANSFORM = provideTransform({ useFactory: packageTransformFactory, deps: [ PROJECT_TOKEN, - OPTIONS_TOKEN, + NX_OPTIONS_TOKEN, INIT_TS_CONFIG_TOKEN, ANALYSE_SOURCES_TOKEN, NX_ENTRY_POINT_TRANSFORM_TOKEN, @@ -43,7 +42,7 @@ export const NX_PACKAGE_TRANSFORM = provideTransform({ export const NX_PACKAGE_PROVIDERS: Provider[] = [ NX_PACKAGE_TRANSFORM, - DEFAULT_OPTIONS_PROVIDER, + NX_DEFAULT_OPTIONS_PROVIDER, INIT_TS_CONFIG_TRANSFORM, ANALYSE_SOURCES_TRANSFORM, ]; diff --git a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/styles/stylesheet-processor.di.ts b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/styles/stylesheet-processor.di.ts new file mode 100644 index 0000000000..868e90db24 --- /dev/null +++ b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/styles/stylesheet-processor.di.ts @@ -0,0 +1,18 @@ +/** + * Adapted from the original ng-packagr source. + * + * Changes made: + * - Use our own StylesheetProcessor instead of the one provided by ng-packagr. + */ + +import { FactoryProvider, InjectionToken } from 'injection-js'; +import { StylesheetProcessor } from './stylesheet-processor'; + +export const NX_STYLESHEET_PROCESSOR_TOKEN = + new InjectionToken(`nx.v1.stylesheetProcessor`); + +export const NX_STYLESHEET_PROCESSOR: FactoryProvider = { + provide: NX_STYLESHEET_PROCESSOR_TOKEN, + useFactory: () => StylesheetProcessor, + deps: [], +}; diff --git a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/styles/stylesheet-processor.ts b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/styles/stylesheet-processor.ts new file mode 100644 index 0000000000..3b02ce742a --- /dev/null +++ b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/styles/stylesheet-processor.ts @@ -0,0 +1,362 @@ +/** + * Adapted from the original ng-packagr source. + * + * Changes made: + * - Added the filePath parameter to the cache key. + * - Refactored caching to take into account TailwindCSS processing. + * - Added PostCSS plugins needed to support TailwindCSS. + * - Added watch mode parameter. + */ + +import * as browserslist from 'browserslist'; +import { sync } from 'find-parent-dir'; +import { existsSync } from 'fs'; +import { EsbuildExecutor } from 'ng-packagr/lib/esbuild/esbuild-executor'; +import { + generateKey, + readCacheEntry, + saveCacheEntry, +} from 'ng-packagr/lib/utils/cache'; +import * as log from 'ng-packagr/lib/utils/log'; +import { dirname, extname, resolve } from 'path'; +import * as postcssPresetEnv from 'postcss-preset-env'; +import * as postcssUrl from 'postcss-url'; +import { + getTailwindPostCssPlugins, + getTailwindSetup, + tailwindDirectives, + TailwindSetup, +} from '../../../utilities/tailwindcss'; + +const postcss = require('postcss'); + +export enum CssUrl { + inline = 'inline', + none = 'none', +} + +export enum InlineStyleLanguage { + sass = 'sass', + scss = 'scss', + css = 'css', + less = 'less', +} + +export interface Result { + css: string; + warnings: string[]; + error?: string; +} + +export class StylesheetProcessor { + private browserslistData: string[]; + private targets: string[]; + private postCssProcessor: ReturnType; + private esbuild = new EsbuildExecutor(); + private tailwindSetup: TailwindSetup | undefined; + + constructor( + private readonly basePath: string, + private readonly cssUrl?: CssUrl, + private readonly styleIncludePaths?: string[], + private readonly cacheDirectory?: string | false, + private readonly watch?: boolean, + private readonly tailwindConfig?: string + ) { + // By default, browserslist defaults are too inclusive + // https://github.com/browserslist/browserslist/blob/83764ea81ffaa39111c204b02c371afa44a4ff07/index.js#L516-L522 + + // We change the default query to browsers that Angular support. + // https://angular.io/guide/browser-support + (browserslist.defaults as string[]) = [ + 'last 1 Chrome version', + 'last 1 Firefox version', + 'last 2 Edge major versions', + 'last 2 Safari major versions', + 'last 2 iOS major versions', + 'Firefox ESR', + ]; + + this.browserslistData = browserslist(undefined, { path: this.basePath }); + this.targets = transformSupportedBrowsersToTargets(this.browserslistData); + this.tailwindSetup = getTailwindSetup(this.basePath, this.tailwindConfig); + this.postCssProcessor = this.createPostCssPlugins(); + } + + async process({ + filePath, + content, + }: { + filePath: string; + content: string; + }): Promise { + let key: string | undefined; + + if ( + this.cacheDirectory && + !content.includes('@import') && + !content.includes('@use') && + !this.containsTailwindDirectives(content) + ) { + // No transitive deps and no Tailwind directives, we can cache more aggressively. + key = await generateKey(content, ...this.browserslistData, filePath); + const result = await readCacheEntry(this.cacheDirectory, key); + if (result) { + result.warnings.forEach((msg) => log.warn(msg)); + + return result.css; + } + } + + // Render pre-processor language (sass, styl, less) + const renderedCss = await this.renderCss(filePath, content); + + let containsTailwindDirectives = false; + if (this.cacheDirectory) { + containsTailwindDirectives = this.containsTailwindDirectives(renderedCss); + if (!containsTailwindDirectives) { + // No Tailwind directives to process by PostCSS, we can return cached results + if (!key) { + key = await generateKey( + renderedCss, + ...this.browserslistData, + filePath + ); + } + + const cachedResult = await this.getCachedResult(key); + if (cachedResult) { + return cachedResult; + } + } + } + + // Render postcss (autoprefixing and friends) + const result = await this.postCssProcessor.process(renderedCss, { + from: filePath, + to: filePath.replace(extname(filePath), '.css'), + }); + + if (this.cacheDirectory && containsTailwindDirectives) { + // We had Tailwind directives to process by PostCSS, only now + // is safe to return cached results + key = await generateKey(result.css, ...this.browserslistData, filePath); + + const cachedResult = await this.getCachedResult(key); + if (cachedResult) { + return cachedResult; + } + } + + const warnings = result.warnings().map((w) => w.toString()); + const { code, warnings: esBuildWarnings } = await this.esbuild.transform( + result.css, + { + loader: 'css', + minify: true, + target: this.targets, + sourcefile: filePath, + } + ); + + if (esBuildWarnings.length > 0) { + warnings.push( + ...(await this.esbuild.formatMessages(esBuildWarnings, { + kind: 'warning', + })) + ); + } + + if (this.cacheDirectory) { + await saveCacheEntry( + this.cacheDirectory, + key, + JSON.stringify({ + css: code, + warnings, + }) + ); + } + warnings.forEach((msg) => log.warn(msg)); + + return code; + } + + private async getCachedResult(key: string): Promise { + const cachedResult = await readCacheEntry( + this.cacheDirectory as string, + key + ); + if (cachedResult) { + cachedResult.warnings.forEach((msg) => log.warn(msg)); + + return cachedResult.css; + } + + return undefined; + } + + private containsTailwindDirectives(content: string): boolean { + return ( + this.tailwindSetup && tailwindDirectives.some((d) => content.includes(d)) + ); + } + + private createPostCssPlugins(): ReturnType { + const postCssPlugins = []; + if (this.cssUrl !== CssUrl.none) { + postCssPlugins.push(postcssUrl({ url: this.cssUrl })); + } + + if (this.tailwindSetup) { + postCssPlugins.push( + ...getTailwindPostCssPlugins( + this.tailwindSetup, + this.styleIncludePaths, + this.watch + ) + ); + } + + postCssPlugins.push( + postcssPresetEnv({ + browsers: this.browserslistData, + autoprefixer: true, + stage: 3, + }) + ); + + return postcss(postCssPlugins); + } + + private async renderCss(filePath: string, css: string): Promise { + const ext = extname(filePath); + + switch (ext) { + case '.sass': + case '.scss': { + return (await import('sass')) + .renderSync({ + file: filePath, + data: css, + indentedSyntax: '.sass' === ext, + importer: customSassImporter, + includePaths: this.styleIncludePaths, + }) + .css.toString(); + } + case '.less': { + const { css: content } = await ( + await import('less') + ).render(css, { + filename: filePath, + math: 'always', + javascriptEnabled: true, + paths: this.styleIncludePaths, + }); + + return content; + } + case '.styl': + case '.stylus': { + const stylus = (await import('stylus')).default; + + return ( + stylus(css) + // add paths for resolve + .set('paths', [ + this.basePath, + '.', + ...this.styleIncludePaths, + 'node_modules', + ]) + // add support for resolving plugins from node_modules + .set('filename', filePath) + // turn on url resolver in stylus, same as flag --resolve-url + .set('resolve url', true) + .define('url', stylus.resolver(undefined)) + .render() + ); + } + case '.css': + default: + return css; + } + } +} + +function transformSupportedBrowsersToTargets( + supportedBrowsers: string[] +): string[] { + const transformed: string[] = []; + + // https://esbuild.github.io/api/#target + const esBuildSupportedBrowsers = new Set([ + 'safari', + 'firefox', + 'edge', + 'chrome', + 'ios', + ]); + + for (const browser of supportedBrowsers) { + let [browserName, version] = browser.split(' '); + + // browserslist uses the name `ios_saf` for iOS Safari whereas esbuild uses `ios` + if (browserName === 'ios_saf') { + browserName = 'ios'; + // browserslist also uses ranges for iOS Safari versions but only the lowest is required + // to perform minimum supported feature checks. esbuild also expects a single version. + [version] = version.split('-'); + } + + if (browserName === 'ie') { + transformed.push('edge12'); + } else if (esBuildSupportedBrowsers.has(browserName)) { + if (browserName === 'safari' && version === 'TP') { + // esbuild only supports numeric versions so `TP` is converted to a high number (999) since + // a Technology Preview (TP) of Safari is assumed to support all currently known features. + version = '999'; + } + + transformed.push(browserName + version); + } + } + + return transformed.length ? transformed : undefined; +} + +function customSassImporter( + url: string, + prev: string +): { file: string; prev: string } | undefined { + // NB: Sass importer should always be sync as otherwise it will cause + // sass to go in the async path which is slower. + if (url[0] !== '~') { + return undefined; + } + + const result = resolveImport(url.substring(1), prev); + if (!result) { + return undefined; + } + + return { + file: result, + prev, + }; +} + +function resolveImport(target: string, basePath: string): string | undefined { + const root = sync(basePath, 'node_modules'); + if (!root) { + return undefined; + } + + const filePath = resolve(root, 'node_modules', target); + if (existsSync(filePath) || existsSync(dirname(filePath))) { + return filePath; + } + + return resolveImport(target, dirname(root)); +} diff --git a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-lite.impl.spec.ts b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-lite.impl.spec.ts index 4fec706def..1da41aa47f 100644 --- a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-lite.impl.spec.ts +++ b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-lite.impl.spec.ts @@ -1,6 +1,7 @@ jest.mock('@nrwl/workspace/src/core/project-graph'); jest.mock('@nrwl/workspace/src/utilities/buildable-libs-utils'); jest.mock('ng-packagr'); +jest.mock('./ng-packagr-adjustments/ng-package/options.di'); import type { ExecutorContext } from '@nrwl/devkit'; import * as buildableLibsUtils from '@nrwl/workspace/src/utilities/buildable-libs-utils'; @@ -8,11 +9,12 @@ import * as ngPackagr from 'ng-packagr'; import { BehaviorSubject } from 'rxjs'; import type { BuildAngularLibraryExecutorOptions } from '../package/schema'; import { NX_ENTRY_POINT_PROVIDERS } from './ng-packagr-adjustments/ng-package/entry-point/entry-point.di'; +import { nxProvideOptions } from './ng-packagr-adjustments/ng-package/options.di'; import { NX_PACKAGE_PROVIDERS, NX_PACKAGE_TRANSFORM, } from './ng-packagr-adjustments/ng-package/package.di'; -import ngPackagrLiteExecutor from './ng-packagr-lite.impl'; +import { ngPackagrLiteExecutor } from './ng-packagr-lite.impl'; describe('NgPackagrLite executor', () => { let context: ExecutorContext; @@ -83,12 +85,24 @@ describe('NgPackagrLite executor', () => { ( buildableLibsUtils.checkDependentProjectsHaveBeenBuilt as jest.Mock ).mockReturnValue(true); + const extraOptions: Partial = { + tailwindConfig: 'path/to/tailwind.config.js', + watch: false, + }; + const nxProvideOptionsResult = { ...extraOptions, cacheEnabled: true }; + (nxProvideOptions as jest.Mock).mockImplementation( + () => nxProvideOptionsResult + ); - const result = await ngPackagrLiteExecutor(options, context).next(); + const result = await ngPackagrLiteExecutor( + { ...options, ...extraOptions }, + context + ).next(); expect(ngPackagr.NgPackagr).toHaveBeenCalledWith([ ...NX_PACKAGE_PROVIDERS, ...NX_ENTRY_POINT_PROVIDERS, + nxProvideOptionsResult, ]); expect(ngPackagrWithBuildTransformMock).toHaveBeenCalledWith( NX_PACKAGE_TRANSFORM.provide diff --git a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-lite.impl.ts b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-lite.impl.ts index 584ce2a8da..bb95ef66b3 100644 --- a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-lite.impl.ts +++ b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-lite.impl.ts @@ -8,6 +8,7 @@ import { resolve } from 'path'; import { createLibraryExecutor } from '../package/package.impl'; import type { BuildAngularLibraryExecutorOptions } from '../package/schema'; import { NX_ENTRY_POINT_PROVIDERS } from './ng-packagr-adjustments/ng-package/entry-point/entry-point.di'; +import { nxProvideOptions } from './ng-packagr-adjustments/ng-package/options.di'; import { NX_PACKAGE_PROVIDERS, NX_PACKAGE_TRANSFORM, @@ -22,6 +23,10 @@ async function initializeNgPackgrLite( // Add default providers to this list. ...NX_PACKAGE_PROVIDERS, ...NX_ENTRY_POINT_PROVIDERS, + nxProvideOptions({ + tailwindConfig: options.tailwindConfig, + watch: options.watch, + }), ]); packager.forProject(resolve(context.root, options.project)); packager.withBuildTransform(NX_PACKAGE_TRANSFORM.provide); diff --git a/packages/angular/src/executors/ng-packagr-lite/schema.json b/packages/angular/src/executors/ng-packagr-lite/schema.json index fc42fedf7b..f1f15dc2ce 100644 --- a/packages/angular/src/executors/ng-packagr-lite/schema.json +++ b/packages/angular/src/executors/ng-packagr-lite/schema.json @@ -28,6 +28,10 @@ "description": "When `updateBuildableProjectDepsInPackageJson` is `true`, this adds dependencies to either `peerDependencies` or `dependencies`.", "enum": ["dependencies", "peerDependencies"], "default": "peerDependencies" + }, + "tailwindConfig": { + "type": "string", + "description": "The full path for the Tailwind configuration file, relative to the workspace root. If not provided and a `tailwind.config.js` file exists in the project or workspace root, it will be used. Otherwise, Tailwind will not be configured." } }, "additionalProperties": false, diff --git a/packages/angular/src/executors/package/ng-packagr-adjustments/ng-package/entry-point/compile-ngc.di.ts b/packages/angular/src/executors/package/ng-packagr-adjustments/ng-package/entry-point/compile-ngc.di.ts index 05493c95b2..a0ea7cf921 100644 --- a/packages/angular/src/executors/package/ng-packagr-adjustments/ng-package/entry-point/compile-ngc.di.ts +++ b/packages/angular/src/executors/package/ng-packagr-adjustments/ng-package/entry-point/compile-ngc.di.ts @@ -13,11 +13,11 @@ import { provideTransform, TransformProvider, } from 'ng-packagr/lib/graph/transform.di'; -import { OPTIONS_TOKEN } from 'ng-packagr/lib/ng-package/options.di'; import { NX_STYLESHEET_PROCESSOR, NX_STYLESHEET_PROCESSOR_TOKEN, } from '../../styles/stylesheet-processor.di'; +import { NX_OPTIONS_TOKEN } from '../options.di'; import { compileNgcTransformFactory } from './compile-ngc.transform'; export const NX_COMPILE_NGC_TOKEN = new InjectionToken( @@ -27,7 +27,7 @@ export const NX_COMPILE_NGC_TOKEN = new InjectionToken( export const NX_COMPILE_NGC_TRANSFORM: TransformProvider = provideTransform({ provide: NX_COMPILE_NGC_TOKEN, useFactory: compileNgcTransformFactory, - deps: [NX_STYLESHEET_PROCESSOR_TOKEN, OPTIONS_TOKEN], + deps: [NX_STYLESHEET_PROCESSOR_TOKEN, NX_OPTIONS_TOKEN], }); export const NX_COMPILE_NGC_PROVIDERS: Provider[] = [ diff --git a/packages/angular/src/executors/package/ng-packagr-adjustments/ng-package/entry-point/compile-ngc.transform.ts b/packages/angular/src/executors/package/ng-packagr-adjustments/ng-package/entry-point/compile-ngc.transform.ts index 2139981fec..7787be3fb7 100644 --- a/packages/angular/src/executors/package/ng-packagr-adjustments/ng-package/entry-point/compile-ngc.transform.ts +++ b/packages/angular/src/executors/package/ng-packagr-adjustments/ng-package/entry-point/compile-ngc.transform.ts @@ -14,7 +14,6 @@ import { isEntryPoint, isEntryPointInProgress, } from 'ng-packagr/lib/ng-package/nodes'; -import { NgPackagrOptions } from 'ng-packagr/lib/ng-package/options.di'; import { compileSourceFiles } from 'ng-packagr/lib/ngc/compile-source-files'; import { NgccProcessor } from 'ng-packagr/lib/ngc/ngcc-processor'; import { setDependenciesTsConfigPaths } from 'ng-packagr/lib/ts/tsconfig'; @@ -23,6 +22,7 @@ import * as ora from 'ora'; import * as path from 'path'; import * as ts from 'typescript'; import { StylesheetProcessor as StylesheetProcessorClass } from '../../styles/stylesheet-processor'; +import { NgPackagrOptions } from '../options.di'; export const compileNgcTransformFactory = ( StylesheetProcessor: typeof StylesheetProcessorClass, @@ -71,7 +71,8 @@ export const compileNgcTransformFactory = ( cssUrl, styleIncludePaths, options.cacheEnabled && options.cacheDirectory, - options.watch + options.watch, + options.tailwindConfig ) as any; await compileSourceFiles( diff --git a/packages/angular/src/executors/package/ng-packagr-adjustments/ng-package/options.di.ts b/packages/angular/src/executors/package/ng-packagr-adjustments/ng-package/options.di.ts new file mode 100644 index 0000000000..bcaaeba992 --- /dev/null +++ b/packages/angular/src/executors/package/ng-packagr-adjustments/ng-package/options.di.ts @@ -0,0 +1,47 @@ +/** + * Adapted from the original ng-packagr. + * + * Changes made: + * - Use our own options interface to add support for tailwindConfig. + */ + +import * as findCacheDirectory from 'find-cache-dir'; +import { InjectionToken, Provider, ValueProvider } from 'injection-js'; +import { NgPackagrOptions as NgPackagrOptionsBase } from 'ng-packagr/lib/ng-package/options.di'; +import { tmpdir } from 'os'; +import { resolve } from 'path'; + +export interface NgPackagrOptions extends NgPackagrOptionsBase { + tailwindConfig?: string; +} + +export const NX_OPTIONS_TOKEN = new InjectionToken( + `nx.v1.options` +); + +export const nxProvideOptions = ( + options: NgPackagrOptions = {} +): ValueProvider => ({ + provide: NX_OPTIONS_TOKEN, + useValue: normalizeOptions(options), +}); + +export const NX_DEFAULT_OPTIONS_PROVIDER: Provider = nxProvideOptions(); + +function normalizeOptions(options: NgPackagrOptions = {}) { + const ciEnv = process.env['CI']; + const isCI = ciEnv?.toLowerCase() === 'true' || ciEnv === '1'; + const { cacheEnabled = !isCI, cacheDirectory = findCachePath() } = options; + + return { + ...options, + cacheEnabled, + cacheDirectory, + }; +} + +function findCachePath(): string { + const name = 'ng-packagr'; + + return findCacheDirectory({ name }) || resolve(tmpdir(), name); +} diff --git a/packages/angular/src/executors/package/ng-packagr-adjustments/ng-package/package.di.ts b/packages/angular/src/executors/package/ng-packagr-adjustments/ng-package/package.di.ts index 0e46a13208..084c807672 100644 --- a/packages/angular/src/executors/package/ng-packagr-adjustments/ng-package/package.di.ts +++ b/packages/angular/src/executors/package/ng-packagr-adjustments/ng-package/package.di.ts @@ -3,6 +3,8 @@ * * Changes made: * - Use NX_ENTRY_POINT_TRANSFORM_TOKEN instead of ENTRY_POINT_TRANSFORM_TOKEN. + * - USE NX_OPTIONS_TOKEN instead of OPTIONS_TOKEN. + * - USE NX_DEFAULT_OPTIONS_PROVIDER instead of DEFAULT_OPTIONS_PROVIDER. */ import { InjectionToken, Provider } from 'injection-js'; @@ -19,13 +21,10 @@ import { INIT_TS_CONFIG_TOKEN, INIT_TS_CONFIG_TRANSFORM, } from 'ng-packagr/lib/ng-package/entry-point/init-tsconfig.di'; -import { - DEFAULT_OPTIONS_PROVIDER, - OPTIONS_TOKEN, -} from 'ng-packagr/lib/ng-package/options.di'; import { packageTransformFactory } from 'ng-packagr/lib/ng-package/package.transform'; import { PROJECT_TOKEN } from 'ng-packagr/lib/project.di'; import { NX_ENTRY_POINT_TRANSFORM_TOKEN } from './entry-point/entry-point.di'; +import { NX_DEFAULT_OPTIONS_PROVIDER, NX_OPTIONS_TOKEN } from './options.di'; export const NX_PACKAGE_TRANSFORM_TOKEN = new InjectionToken( `nx.v1.packageTransform` @@ -36,7 +35,7 @@ export const NX_PACKAGE_TRANSFORM: TransformProvider = provideTransform({ useFactory: packageTransformFactory, deps: [ PROJECT_TOKEN, - OPTIONS_TOKEN, + NX_OPTIONS_TOKEN, INIT_TS_CONFIG_TOKEN, ANALYSE_SOURCES_TOKEN, NX_ENTRY_POINT_TRANSFORM_TOKEN, @@ -45,7 +44,7 @@ export const NX_PACKAGE_TRANSFORM: TransformProvider = provideTransform({ export const NX_PACKAGE_PROVIDERS: Provider[] = [ NX_PACKAGE_TRANSFORM, - DEFAULT_OPTIONS_PROVIDER, + NX_DEFAULT_OPTIONS_PROVIDER, INIT_TS_CONFIG_TRANSFORM, ANALYSE_SOURCES_TRANSFORM, ]; diff --git a/packages/angular/src/executors/package/ng-packagr-adjustments/styles/stylesheet-processor.ts b/packages/angular/src/executors/package/ng-packagr-adjustments/styles/stylesheet-processor.ts index 387c0dcfaf..3b02ce742a 100644 --- a/packages/angular/src/executors/package/ng-packagr-adjustments/styles/stylesheet-processor.ts +++ b/packages/angular/src/executors/package/ng-packagr-adjustments/styles/stylesheet-processor.ts @@ -26,7 +26,7 @@ import { getTailwindSetup, tailwindDirectives, TailwindSetup, -} from '../utilities/tailwindcss'; +} from '../../../utilities/tailwindcss'; const postcss = require('postcss'); @@ -60,7 +60,8 @@ export class StylesheetProcessor { private readonly cssUrl?: CssUrl, private readonly styleIncludePaths?: string[], private readonly cacheDirectory?: string | false, - private readonly watch?: boolean + private readonly watch?: boolean, + private readonly tailwindConfig?: string ) { // By default, browserslist defaults are too inclusive // https://github.com/browserslist/browserslist/blob/83764ea81ffaa39111c204b02c371afa44a4ff07/index.js#L516-L522 @@ -78,7 +79,7 @@ export class StylesheetProcessor { this.browserslistData = browserslist(undefined, { path: this.basePath }); this.targets = transformSupportedBrowsersToTargets(this.browserslistData); - this.tailwindSetup = getTailwindSetup(this.basePath); + this.tailwindSetup = getTailwindSetup(this.basePath, this.tailwindConfig); this.postCssProcessor = this.createPostCssPlugins(); } diff --git a/packages/angular/src/executors/package/package.impl.spec.ts b/packages/angular/src/executors/package/package.impl.spec.ts index 023066f129..31710978fc 100644 --- a/packages/angular/src/executors/package/package.impl.spec.ts +++ b/packages/angular/src/executors/package/package.impl.spec.ts @@ -1,17 +1,19 @@ jest.mock('@nrwl/workspace/src/core/project-graph'); jest.mock('@nrwl/workspace/src/utilities/buildable-libs-utils'); jest.mock('ng-packagr'); +jest.mock('./ng-packagr-adjustments/ng-package/options.di'); import type { ExecutorContext } from '@nrwl/devkit'; import * as buildableLibsUtils from '@nrwl/workspace/src/utilities/buildable-libs-utils'; import * as ngPackagr from 'ng-packagr'; import { BehaviorSubject } from 'rxjs'; import { NX_ENTRY_POINT_PROVIDERS } from './ng-packagr-adjustments/ng-package/entry-point/entry-point.di'; +import { nxProvideOptions } from './ng-packagr-adjustments/ng-package/options.di'; import { NX_PACKAGE_PROVIDERS, NX_PACKAGE_TRANSFORM, } from './ng-packagr-adjustments/ng-package/package.di'; -import packageExecutor from './package.impl'; +import { packageExecutor } from './package.impl'; import type { BuildAngularLibraryExecutorOptions } from './schema'; describe('Package executor', () => { @@ -82,12 +84,24 @@ describe('Package executor', () => { ( buildableLibsUtils.checkDependentProjectsHaveBeenBuilt as jest.Mock ).mockReturnValue(true); + const extraOptions: Partial = { + tailwindConfig: 'path/to/tailwind.config.js', + watch: false, + }; + const nxProvideOptionsResult = { ...extraOptions, cacheEnabled: true }; + (nxProvideOptions as jest.Mock).mockImplementation( + () => nxProvideOptionsResult + ); - const result = await packageExecutor(options, context).next(); + const result = await packageExecutor( + { ...options, ...extraOptions }, + context + ).next(); expect(ngPackagr.NgPackagr).toHaveBeenCalledWith([ ...NX_PACKAGE_PROVIDERS, ...NX_ENTRY_POINT_PROVIDERS, + nxProvideOptionsResult, ]); expect(ngPackagrWithBuildTransformMock).toHaveBeenCalledWith( NX_PACKAGE_TRANSFORM.provide diff --git a/packages/angular/src/executors/package/package.impl.ts b/packages/angular/src/executors/package/package.impl.ts index 2c876041d7..c8b3cba34f 100644 --- a/packages/angular/src/executors/package/package.impl.ts +++ b/packages/angular/src/executors/package/package.impl.ts @@ -1,12 +1,10 @@ import type { ExecutorContext } from '@nrwl/devkit'; import { readCachedProjectGraph } from '@nrwl/workspace/src/core/project-graph'; -import { - createTmpTsConfig, - DependentBuildableProjectNode, -} from '@nrwl/workspace/src/utilities/buildable-libs-utils'; import { calculateProjectDependencies, checkDependentProjectsHaveBeenBuilt, + createTmpTsConfig, + DependentBuildableProjectNode, updateBuildableProjectPackageJsonDependencies, } from '@nrwl/workspace/src/utilities/buildable-libs-utils'; import type { NgPackagr } from 'ng-packagr'; @@ -15,6 +13,7 @@ import { from } from 'rxjs'; import { eachValueFrom } from 'rxjs-for-await'; import { mapTo, switchMap, tap } from 'rxjs/operators'; import { NX_ENTRY_POINT_PROVIDERS } from './ng-packagr-adjustments/ng-package/entry-point/entry-point.di'; +import { nxProvideOptions } from './ng-packagr-adjustments/ng-package/options.di'; import { NX_PACKAGE_PROVIDERS, NX_PACKAGE_TRANSFORM, @@ -29,6 +28,10 @@ async function initializeNgPackagr( const packager = new (await import('ng-packagr')).NgPackagr([ ...NX_PACKAGE_PROVIDERS, ...NX_ENTRY_POINT_PROVIDERS, + nxProvideOptions({ + tailwindConfig: options.tailwindConfig, + watch: options.watch, + }), ]); packager.forProject(resolve(context.root, options.project)); diff --git a/packages/angular/src/executors/package/schema.d.ts b/packages/angular/src/executors/package/schema.d.ts index a9ab6080d2..abe918b1ac 100644 --- a/packages/angular/src/executors/package/schema.d.ts +++ b/packages/angular/src/executors/package/schema.d.ts @@ -1,7 +1,8 @@ export interface BuildAngularLibraryExecutorOptions { project: string; - tsConfig?: string; buildableProjectDepsInPackageJsonType?: 'dependencies' | 'peerDependencies'; + tailwindConfig?: string; + tsConfig?: string; updateBuildableProjectDepsInPackageJson?: boolean; watch?: boolean; } diff --git a/packages/angular/src/executors/package/schema.json b/packages/angular/src/executors/package/schema.json index 1fbb9e52d7..212deeece8 100644 --- a/packages/angular/src/executors/package/schema.json +++ b/packages/angular/src/executors/package/schema.json @@ -28,6 +28,10 @@ "description": "When `updateBuildableProjectDepsInPackageJson` is `true`, this adds dependencies to either `peerDependencies` or `dependencies`.", "enum": ["dependencies", "peerDependencies"], "default": "peerDependencies" + }, + "tailwindConfig": { + "type": "string", + "description": "The full path for the Tailwind configuration file, relative to the workspace root. If not provided and a `tailwind.config.js` file exists in the project or workspace root, it will be used. Otherwise, Tailwind will not be configured." } }, "additionalProperties": false, diff --git a/packages/angular/src/executors/package/ng-packagr-adjustments/utilities/tailwindcss.ts b/packages/angular/src/executors/utilities/tailwindcss.ts similarity index 76% rename from packages/angular/src/executors/package/ng-packagr-adjustments/utilities/tailwindcss.ts rename to packages/angular/src/executors/utilities/tailwindcss.ts index 9de25ea672..cd712fa624 100644 --- a/packages/angular/src/executors/package/ng-packagr-adjustments/utilities/tailwindcss.ts +++ b/packages/angular/src/executors/utilities/tailwindcss.ts @@ -18,15 +18,21 @@ export const tailwindDirectives = [ '@screen', ]; -export function getTailwindSetup(basePath: string): TailwindSetup | undefined { - // Try to find TailwindCSS configuration file in the project or workspace root. - const tailwindConfigFile = 'tailwind.config.js'; - let tailwindConfigPath: string | undefined; - for (const path of [basePath, appRootPath]) { - const fullPath = join(path, tailwindConfigFile); - if (existsSync(fullPath)) { - tailwindConfigPath = fullPath; - break; +export function getTailwindSetup( + basePath: string, + tailwindConfig?: string +): TailwindSetup | undefined { + let tailwindConfigPath = tailwindConfig; + + if (!tailwindConfigPath) { + // Try to find TailwindCSS configuration file in the project or workspace root. + const tailwindConfigFile = 'tailwind.config.js'; + for (const path of [basePath, appRootPath]) { + const fullPath = join(path, tailwindConfigFile); + if (existsSync(fullPath)) { + tailwindConfigPath = fullPath; + break; + } } }