feat(angular): add tailwind support for buildable libraries (#7961)

This commit is contained in:
Leosvel Pérez Espinosa 2021-12-02 11:16:01 +00:00 committed by GitHub
parent 56aaeb7931
commit 9a08a83591
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 886 additions and 90 deletions

View File

@ -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`

View File

@ -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`

View File

@ -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`

View File

@ -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`

View File

@ -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`

View File

@ -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`

View File

@ -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: '<button class="custom-btn">Click me!</button>',
styles: [\`
.custom-btn {
@apply m-md p-sm;
}
\`]
})
export class FooComponent {}
`
);
updateFile(
`libs/${lib}/src/lib/${lib}.module.ts`,
`import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FooComponent } from './foo.component';
@NgModule({
imports: [CommonModule],
declarations: [FooComponent],
exports: [FooComponent],
})
export class LibModule {}
`
);
updateFile(
`libs/${lib}/src/index.ts`,
`export * from './lib/foo.component';
export * from './lib/${lib}.module';
`
);
};
beforeEach(() => {
const projectName = uniq('proj');
projectScope = newProject({ name: projectName });
buildLibExecutorOption = uniq('build-lib-executor-option');
buildLibProjectConfig = uniq('build-lib-project-config');
buildLibRootConfig = uniq('build-lib-root-config');
pubLibExecutorOption = uniq('pub-lib-executor-option');
pubLibProjectConfig = uniq('pub-lib-project-config');
pubLibRootConfig = uniq('pub-lib-root-config');
// Install Tailwind required packages.
// TODO: Remove this when Tailwind generator is created and used.
packageInstall('tailwindcss postcss autoprefixer', projectName, 'latest');
// Create Tailwind config in the workspace root.
createWorkspaceTailwindConfigFile();
// Setup buildable libs
// Buildable lib with tailwind config specified in the project config
runCLI(
`generate @nrwl/angular:lib ${buildLibExecutorOption} --buildable --no-interactive`
);
createLibComponent(buildLibExecutorOption);
createTailwindConfigFile(
`libs/${buildLibExecutorOption}`,
buildLibExecutorOption,
spacing.executorOption,
customTailwindConfigFile
);
addTailwindConfigToProject(buildLibExecutorOption);
// Buildable lib with tailwind config located in the project root
runCLI(
`generate @nrwl/angular:lib ${buildLibProjectConfig} --buildable --no-interactive`
);
createLibComponent(buildLibProjectConfig);
createTailwindConfigFile(
`libs/${buildLibProjectConfig}`,
buildLibProjectConfig,
spacing.projectConfig
);
// Buildable lib with tailwind config located in the workspace root
runCLI(
`generate @nrwl/angular:lib ${buildLibRootConfig} --buildable --no-interactive`
);
createLibComponent(buildLibRootConfig);
// Publishable libs
// Publishable lib with tailwind config specified in the project config
runCLI(
`generate @nrwl/angular:lib ${pubLibExecutorOption} --publishable --importPath=@${projectScope}/${pubLibExecutorOption} --no-interactive`
);
createLibComponent(pubLibExecutorOption);
createTailwindConfigFile(
`libs/${pubLibExecutorOption}`,
pubLibExecutorOption,
spacing.executorOption,
customTailwindConfigFile
);
addTailwindConfigToProject(pubLibExecutorOption);
// Publishable lib with tailwind config located in the project root
runCLI(
`generate @nrwl/angular:lib ${pubLibProjectConfig} --publishable --importPath=@${projectScope}/${pubLibProjectConfig} --no-interactive`
);
createLibComponent(pubLibProjectConfig);
createTailwindConfigFile(
`libs/${pubLibProjectConfig}`,
pubLibProjectConfig,
spacing.projectConfig
);
// Publishable lib with tailwind config located in the workspace root
runCLI(
`generate @nrwl/angular:lib ${pubLibRootConfig} --publishable --importPath=@${projectScope}/${pubLibRootConfig} --no-interactive`
);
createLibComponent(pubLibRootConfig);
});
const assertComponentStyles = (
lib: string,
libSpacing: typeof spacing['executorOption']
) => {
const builtComponentContent = readFile(
`dist/libs/${lib}/esm2020/lib/foo.component.mjs`
);
let expectedStylesRegex = new RegExp(
`styles: \\[\\"\\.custom\\-btn(\\[_ngcontent\\-%COMP%\\])?{margin:${libSpacing.md};padding:${libSpacing.sm}}(\\\\n)?\\"\\]`
);
expect(builtComponentContent).toMatch(expectedStylesRegex);
};
it('should build and output the right styles based on the tailwind config', () => {
runCLI(`build ${buildLibExecutorOption}`);
assertComponentStyles(buildLibExecutorOption, spacing.executorOption);
runCLI(`build ${buildLibProjectConfig}`);
assertComponentStyles(buildLibProjectConfig, spacing.projectConfig);
runCLI(`build ${buildLibRootConfig}`);
assertComponentStyles(buildLibRootConfig, spacing.rootConfig);
runCLI(`build ${pubLibExecutorOption}`);
assertComponentStyles(pubLibExecutorOption, spacing.executorOption);
runCLI(`build ${pubLibProjectConfig}`);
assertComponentStyles(pubLibProjectConfig, spacing.projectConfig);
runCLI(`build ${pubLibRootConfig}`);
assertComponentStyles(pubLibRootConfig, spacing.rootConfig);
});
});
});

View File

@ -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<Transform>(
@ -14,9 +24,9 @@ export const NX_COMPILE_NGC_TOKEN = new InjectionToken<Transform>(
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,
];

View File

@ -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,

View File

@ -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<Transform>(
@ -20,5 +21,5 @@ export const NX_WRITE_PACKAGE_TRANSFORM_TOKEN = new InjectionToken<Transform>(
export const NX_WRITE_PACKAGE_TRANSFORM: TransformProvider = provideTransform({
provide: NX_WRITE_PACKAGE_TRANSFORM_TOKEN,
useFactory: nxWritePackageTransform,
deps: [OPTIONS_TOKEN],
deps: [NX_OPTIONS_TOKEN],
});

View File

@ -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<NgPackagrOptions>(
`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);
}

View File

@ -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<Transform>(
`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,
];

View File

@ -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<StylesheetProcessor>(`nx.v1.stylesheetProcessor`);
export const NX_STYLESHEET_PROCESSOR: FactoryProvider = {
provide: NX_STYLESHEET_PROCESSOR_TOKEN,
useFactory: () => StylesheetProcessor,
deps: [],
};

View File

@ -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<typeof postcss>;
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<string> {
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<string | undefined> {
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<typeof postcss> {
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<string> {
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));
}

View File

@ -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<BuildAngularLibraryExecutorOptions> = {
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

View File

@ -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);

View File

@ -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,

View File

@ -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<Transform>(
@ -27,7 +27,7 @@ export const NX_COMPILE_NGC_TOKEN = new InjectionToken<Transform>(
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[] = [

View File

@ -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(

View File

@ -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<NgPackagrOptions>(
`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);
}

View File

@ -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<Transform>(
`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,
];

View File

@ -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();
}

View File

@ -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<BuildAngularLibraryExecutorOptions> = {
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

View File

@ -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));

View File

@ -1,7 +1,8 @@
export interface BuildAngularLibraryExecutorOptions {
project: string;
tsConfig?: string;
buildableProjectDepsInPackageJsonType?: 'dependencies' | 'peerDependencies';
tailwindConfig?: string;
tsConfig?: string;
updateBuildableProjectDepsInPackageJson?: boolean;
watch?: boolean;
}

View File

@ -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,

View File

@ -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;
}
}
}