diff --git a/package.json b/package.json index 5cdb92a3d7..622514dcff 100644 --- a/package.json +++ b/package.json @@ -184,7 +184,7 @@ "minimatch": "3.0.4", "next": "^11.1.2", "next-sitemap": "^1.6.108", - "ng-packagr": "~12.2.0", + "ng-packagr": "~12.2.3", "ngrx-store-freeze": "0.2.4", "node-fetch": "^2.6.1", "npm-run-all": "^4.1.5", @@ -193,6 +193,7 @@ "parse5": "4.0.0", "postcss": "8.3.0", "postcss-import": "14.0.2", + "postcss-url": "^10.1.1", "precise-commits": "1.0.2", "prettier": "2.3.2", "pretty-quick": "^3.1.0", diff --git a/packages/angular/migrations.json b/packages/angular/migrations.json index 766a62cc5f..4aa7086d29 100644 --- a/packages/angular/migrations.json +++ b/packages/angular/migrations.json @@ -90,6 +90,12 @@ "version": "12.9.0", "description": "Fixes invalid importPaths for buildable and publishable libs.", "factory": "./src/migrations/update-12-9-0/update-invalid-import-paths" + }, + "add-postcss-import": { + "cli": "nx", + "version": "13.0.0-beta.1", + "description": "Adds postcss-import package if ng-packagr is already installed.", + "factory": "./src/migrations/update-13-0-0/add-postcss-import" } }, "packageJsonUpdates": { @@ -880,6 +886,15 @@ "alwaysAddToPackageJson": false } } + }, + "13.0.0": { + "version": "13.0.0-beta.1", + "packages": { + "ng-packagr": { + "version": "~12.2.3", + "alwaysAddToPackageJson": false + } + } } } } diff --git a/packages/angular/src/executors/package/ng-packagr-adjustments/ivy/styles/stylesheet-processor.ts b/packages/angular/src/executors/package/ng-packagr-adjustments/ivy/styles/stylesheet-processor.ts new file mode 100644 index 0000000000..81156b8346 --- /dev/null +++ b/packages/angular/src/executors/package/ng-packagr-adjustments/ivy/styles/stylesheet-processor.ts @@ -0,0 +1,240 @@ +/** + * Adapted from the original ng-packagr source. + * + * Changes made: + * - Added the filePath parameter to the cache key. + * - Added PostCSS plugins needed to support TailwindCSS. + * - Added watch mode parameter. + */ + +import * as browserslist from 'browserslist'; +import * as cacache from 'cacache'; +import { createHash } from 'crypto'; +import * as findCacheDirectory from 'find-cache-dir'; +import { EsbuildExecutor } from 'ng-packagr/lib/esbuild/esbuild-executor'; +import { readFile } from 'ng-packagr/lib/utils/fs'; +import * as log from 'ng-packagr/lib/utils/log'; +import { tmpdir } from 'os'; +import { extname } from 'path'; +import postcss from 'postcss'; +import * as postcssPresetEnv from 'postcss-preset-env'; +import * as postcssUrl from 'postcss-url'; +import { getTailwindPostCssPluginsIfPresent } from '../../utilities/tailwindcss'; + +export enum CssUrl { + inline = 'inline', + none = 'none', +} + +export interface Result { + css: string; + warnings: string[]; + error?: string; +} + +const cachePath = findCacheDirectory({ name: 'ng-packagr-styles' }) || tmpdir(); +const ngPackagrVersion = require('ng-packagr/package.json').version; + +export class StylesheetProcessor { + private browserslistData: string[]; + private postCssProcessor: ReturnType; + private esbuild = new EsbuildExecutor(); + + constructor( + private readonly basePath: string, + private readonly cssUrl?: CssUrl, + private readonly styleIncludePaths?: string[], + private readonly watch?: boolean + ) { + this.browserslistData = browserslist(undefined, { path: this.basePath }); + this.postCssProcessor = this.createPostCssPlugins(); + } + + async process(filePath: string): Promise { + const content = await readFile(filePath, 'utf8'); + let key: string | undefined; + + if (!content.includes('@import') && !content.includes('@use')) { + // No transitive deps, we can cache more aggressively. + key = generateKey(content, this.browserslistData, filePath); + const result = await readCacheEntry(cachePath, 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); + + // We cannot cache CSS re-rendering phase, because a transitive dependency via (@import) can case different CSS output. + // Example a change in a mixin or SCSS variable. + if (!key) { + key = generateKey(renderedCss, this.browserslistData, filePath); + } + + const cachedResult = await readCacheEntry(cachePath, key); + if (cachedResult) { + cachedResult.warnings.forEach((msg) => log.warn(msg)); + return cachedResult.css; + } + + // Render postcss (autoprefixing and friends) + const result = await this.postCssProcessor.process(renderedCss, { + from: filePath, + to: filePath.replace(extname(filePath), '.css'), + }); + + const warnings = result.warnings().map((w) => w.toString()); + const { code, warnings: esBuildWarnings } = await this.esbuild.transform( + result.css, + { + loader: 'css', + minify: true, + sourcefile: filePath, + } + ); + + if (esBuildWarnings.length > 0) { + warnings.push( + ...(await this.esbuild.formatMessages(esBuildWarnings, { + kind: 'warning', + })) + ); + } + + // Add to cache + await cacache.put( + cachePath, + key, + JSON.stringify({ + css: code, + warnings, + }) + ); + + warnings.forEach((msg) => log.warn(msg)); + + return code; + } + + private createPostCssPlugins(): ReturnType { + const postCssPlugins = []; + if (this.cssUrl !== CssUrl.none) { + postCssPlugins.push(postcssUrl({ url: this.cssUrl })); + } + + postCssPlugins.push( + ...getTailwindPostCssPluginsIfPresent( + this.basePath, + 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': { + /* + * Please be aware of the few differences in behaviour https://github.com/sass/dart-sass/blob/master/README.md#behavioral-differences-from-ruby-sass + * By default `npm install` will install sass. + * To use node-sass you need to use: + * Npm: + * `npm install node-sass --save-dev` + * Yarn: + * `yarn add node-sass --dev` + */ + let sassCompiler: any | undefined; + try { + sassCompiler = require('node-sass'); // Check if node-sass is explicitly included. + } catch { + sassCompiler = await import('sass'); + } + + return sassCompiler + .renderSync({ + file: filePath, + data: css, + indentedSyntax: '.sass' === ext, + importer: await import('node-sass-tilde-importer'), + includePaths: this.styleIncludePaths, + }) + .css.toString(); + } + case '.less': { + const { css: content } = await ( + await import('less') + ).render(css, { + filename: filePath, + javascriptEnabled: true, + paths: this.styleIncludePaths, + }); + + return content; + } + case '.styl': + case '.stylus': { + const stylus = await import('stylus'); + + 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 generateKey( + content: string, + browserslistData: string[], + filePath: string +): string { + return createHash('sha1') + .update(ngPackagrVersion) + .update(content) + .update(browserslistData.join('')) + .update(filePath) + .digest('hex'); +} + +async function readCacheEntry( + cachePath: string, + key: string +): Promise { + const entry = await cacache.get.info(cachePath, key); + if (entry) { + return JSON.parse(await readFile(entry.path, 'utf8')); + } + + return undefined; +} 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 new file mode 100644 index 0000000000..05493c95b2 --- /dev/null +++ b/packages/angular/src/executors/package/ng-packagr-adjustments/ng-package/entry-point/compile-ngc.di.ts @@ -0,0 +1,36 @@ +/** + * 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. + */ + +import { InjectionToken, Provider } from 'injection-js'; +import { Transform } from 'ng-packagr/lib/graph/transform'; +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 { compileNgcTransformFactory } from './compile-ngc.transform'; + +export const NX_COMPILE_NGC_TOKEN = new InjectionToken( + `nx.v1.compileNgcTransform` +); + +export const NX_COMPILE_NGC_TRANSFORM: TransformProvider = provideTransform({ + provide: NX_COMPILE_NGC_TOKEN, + useFactory: compileNgcTransformFactory, + deps: [NX_STYLESHEET_PROCESSOR_TOKEN, OPTIONS_TOKEN], +}); + +export const NX_COMPILE_NGC_PROVIDERS: Provider[] = [ + NX_STYLESHEET_PROCESSOR, + NX_COMPILE_NGC_TRANSFORM, +]; 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 new file mode 100644 index 0000000000..82abf4f471 --- /dev/null +++ b/packages/angular/src/executors/package/ng-packagr-adjustments/ng-package/entry-point/compile-ngc.transform.ts @@ -0,0 +1,136 @@ +/** + * Adapted from the original ng-packagr source. + * + * Changes made: + * - Use our own StylesheetProcessor files instead of the ones provide by ng-packagr. + */ + +import { + Transform, + transformFromPromise, +} from 'ng-packagr/lib/graph/transform'; +import * as ivy from 'ng-packagr/lib/ivy'; +import { + EntryPointNode, + 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'; +import * as ora from 'ora'; +import * as path from 'path'; +import * as ts from 'typescript'; +import { StylesheetProcessor as IvyStylesheetProcessor } from '../../ivy/styles/stylesheet-processor'; +import { StylesheetProcessor as StylesheetProcessorClass } from '../../styles/stylesheet-processor'; + +function isEnabled(variable: string | undefined): variable is string { + return ( + typeof variable === 'string' && + (variable === '1' || variable.toLowerCase() === 'true') + ); +} + +export const compileNgcTransformFactory = ( + StylesheetProcessor: typeof StylesheetProcessorClass, + options: NgPackagrOptions +): Transform => { + return transformFromPromise(async (graph) => { + const spinner = ora({ + hideCursor: false, + discardStdin: false, + }); + + try { + const entryPoint: EntryPointNode = graph.find(isEntryPointInProgress()); + const entryPoints: EntryPointNode[] = graph.filter(isEntryPoint); + // Add paths mappings for dependencies + const tsConfig = setDependenciesTsConfigPaths( + entryPoint.data.tsConfig, + entryPoints + ); + + // Compile TypeScript sources + const { esm2015, declarations } = entryPoint.data.destinationFiles; + const { basePath, cssUrl, styleIncludePaths } = + entryPoint.data.entryPoint; + const { moduleResolutionCache, ngccProcessingCache } = entryPoint.cache; + + let ngccProcessor: NgccProcessor | undefined; + if (tsConfig.options.enableIvy !== false) { + spinner.start( + `Compiling with Angular sources in Ivy ${ + tsConfig.options.compilationMode || 'full' + } compilation mode.` + ); + ngccProcessor = new NgccProcessor( + ngccProcessingCache, + tsConfig.project, + tsConfig.options, + entryPoints + ); + if (!entryPoint.data.entryPoint.isSecondaryEntryPoint) { + // Only run the async version of NGCC during the primary entrypoint processing. + await ngccProcessor.process(); + } + } else { + spinner.start( + `Compiling with Angular in legacy View Engine compilation mode.` + ); + } + + if ( + tsConfig.options.enableIvy !== false && + !isEnabled(process.env['NG_BUILD_LIB_LEGACY']) + ) { + entryPoint.cache.stylesheetProcessor ??= new IvyStylesheetProcessor( + basePath, + cssUrl, + styleIncludePaths + ) as any; + + await ivy.compileSourceFiles( + graph, + tsConfig, + moduleResolutionCache, + { + outDir: path.dirname(esm2015), + declarationDir: path.dirname(declarations), + declaration: true, + target: ts.ScriptTarget.ES2015, + }, + entryPoint.cache.stylesheetProcessor as any, + ngccProcessor, + options.watch + ); + } else { + entryPoint.cache.stylesheetProcessor ??= new StylesheetProcessor( + basePath, + cssUrl, + styleIncludePaths, + options.watch + ) as any; + await compileSourceFiles( + graph, + tsConfig, + moduleResolutionCache, + entryPoint.cache.stylesheetProcessor as any, + { + outDir: path.dirname(esm2015), + declarationDir: path.dirname(declarations), + declaration: true, + target: ts.ScriptTarget.ES2015, + }, + ngccProcessor + ); + } + } catch (error) { + spinner.fail(); + throw error; + } + + spinner.succeed(); + return graph; + }); +}; diff --git a/packages/angular/src/executors/package/ng-packagr-adjustments/ng-package/entry-point/entry-point.di.ts b/packages/angular/src/executors/package/ng-packagr-adjustments/ng-package/entry-point/entry-point.di.ts new file mode 100644 index 0000000000..d8070cb478 --- /dev/null +++ b/packages/angular/src/executors/package/ng-packagr-adjustments/ng-package/entry-point/entry-point.di.ts @@ -0,0 +1,48 @@ +/** + * Adapted from the original ng-packagr source. + * + * Changes made: + * - Use NX_COMPILE_NGC_TOKEN instead of COMPILE_NGC_TOKEN. + * - Use NX_COMPILE_NGC_PROVIDERS instead of COMPILE_NGC_PROVIDERS. + */ + +import { InjectionToken, Provider } from 'injection-js'; +import { Transform } from 'ng-packagr/lib/graph/transform'; +import { + provideTransform, + TransformProvider, +} from 'ng-packagr/lib/graph/transform.di'; +import { entryPointTransformFactory } from 'ng-packagr/lib/ng-package/entry-point/entry-point.transform'; +import { + WRITE_BUNDLES_TRANSFORM, + WRITE_BUNDLES_TRANSFORM_TOKEN, +} from 'ng-packagr/lib/ng-package/entry-point/write-bundles.di'; +import { + WRITE_PACKAGE_TRANSFORM, + WRITE_PACKAGE_TRANSFORM_TOKEN, +} from 'ng-packagr/lib/ng-package/entry-point/write-package.di'; +import { + NX_COMPILE_NGC_PROVIDERS, + NX_COMPILE_NGC_TOKEN, +} from './compile-ngc.di'; + +export const NX_ENTRY_POINT_TRANSFORM_TOKEN = new InjectionToken( + `nx.v1.entryPointTransform` +); + +export const NX_ENTRY_POINT_TRANSFORM: TransformProvider = provideTransform({ + provide: NX_ENTRY_POINT_TRANSFORM_TOKEN, + useFactory: entryPointTransformFactory, + deps: [ + NX_COMPILE_NGC_TOKEN, + WRITE_BUNDLES_TRANSFORM_TOKEN, + WRITE_PACKAGE_TRANSFORM_TOKEN, + ], +}); + +export const NX_ENTRY_POINT_PROVIDERS: Provider[] = [ + NX_ENTRY_POINT_TRANSFORM, + ...NX_COMPILE_NGC_PROVIDERS, + WRITE_BUNDLES_TRANSFORM, + WRITE_PACKAGE_TRANSFORM, +]; 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 new file mode 100644 index 0000000000..f1c237a0ea --- /dev/null +++ b/packages/angular/src/executors/package/ng-packagr-adjustments/ng-package/package.di.ts @@ -0,0 +1,53 @@ +/** + * Adapted from the original ng-packagr source. + * + * Changes made: + * - Use NX_ENTRY_POINT_TRANSFORM_TOKEN instead of ENTRY_POINT_TRANSFORM_TOKEN. + */ + +import { InjectionToken, Provider } from 'injection-js'; +import { Transform } from 'ng-packagr/lib/graph/transform'; +import { + provideTransform, + TransformProvider, +} from 'ng-packagr/lib/graph/transform.di'; +import { + ANALYSE_SOURCES_TOKEN, + ANALYSE_SOURCES_TRANSFORM, +} from 'ng-packagr/lib/ng-package/entry-point/analyse-sources.di'; +import { + INIT_TS_CONFIG_TOKEN, + INIT_TS_CONFIG_TRANSFORM, + provideTsConfig, +} 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'; + +export const NX_PACKAGE_TRANSFORM_TOKEN = new InjectionToken( + `nx.v1.packageTransform` +); + +export const NX_PACKAGE_TRANSFORM: TransformProvider = provideTransform({ + provide: NX_PACKAGE_TRANSFORM_TOKEN, + useFactory: packageTransformFactory, + deps: [ + PROJECT_TOKEN, + OPTIONS_TOKEN, + INIT_TS_CONFIG_TOKEN, + ANALYSE_SOURCES_TOKEN, + NX_ENTRY_POINT_TRANSFORM_TOKEN, + ], +}); + +export const NX_PACKAGE_PROVIDERS: Provider[] = [ + NX_PACKAGE_TRANSFORM, + DEFAULT_OPTIONS_PROVIDER, + provideTsConfig(), + INIT_TS_CONFIG_TRANSFORM, + ANALYSE_SOURCES_TRANSFORM, +]; diff --git a/packages/angular/src/executors/package/ng-packagr-adjustments/styles/stylesheet-processor-worker.ts b/packages/angular/src/executors/package/ng-packagr-adjustments/styles/stylesheet-processor-worker.ts new file mode 100644 index 0000000000..43723b7ea1 --- /dev/null +++ b/packages/angular/src/executors/package/ng-packagr-adjustments/styles/stylesheet-processor-worker.ts @@ -0,0 +1,250 @@ +/** + * Adapted from the original ng-packagr source. + * + * Changes made: + * - Added the filePath parameter to the cache key. + * - Added PostCSS plugins needed to support TailwindCSS. + * - Added watch mode option. + */ + +import * as cacache from 'cacache'; +import { createHash } from 'crypto'; +import { EsbuildExecutor } from 'ng-packagr/lib/esbuild/esbuild-executor'; +import { readFile } from 'ng-packagr/lib/utils/fs'; +import * as path from 'path'; +import postcss, { LazyResult } from 'postcss'; +import * as postcssPresetEnv from 'postcss-preset-env'; +import * as postcssUrl from 'postcss-url'; +import { parentPort } from 'worker_threads'; +import { getTailwindPostCssPluginsIfPresent } from '../utilities/tailwindcss'; +import { CssUrl, WorkerOptions, WorkerResult } from './stylesheet-processor'; + +const ngPackagrVersion = require('ng-packagr/package.json').version; + +async function processCss({ + filePath, + browserslistData, + cssUrl, + styleIncludePaths, + basePath, + cachePath, + alwaysUseWasm, + watch, +}: WorkerOptions): Promise { + const esbuild = new EsbuildExecutor(alwaysUseWasm); + const content = await readFile(filePath, 'utf8'); + let key: string | undefined; + + if (!content.includes('@import') && !content.includes('@use')) { + // No transitive deps, we can cache more aggressively. + key = generateKey(content, browserslistData, filePath); + const result = await readCacheEntry(cachePath, key); + if (result) { + return result; + } + } + + // Render pre-processor language (sass, styl, less) + const renderedCss = await renderCss( + filePath, + content, + basePath, + styleIncludePaths + ); + + // We cannot cache CSS re-rendering phase, because a transitive dependency via (@import) can case different CSS output. + // Example a change in a mixin or SCSS variable. + if (!key) { + key = generateKey(renderedCss, browserslistData, filePath); + + const cachedResult = await readCacheEntry(cachePath, key); + if (cachedResult) { + return cachedResult; + } + } + + // Render postcss (autoprefixing and friends) + const result = await optimizeCss( + filePath, + renderedCss, + browserslistData, + basePath, + styleIncludePaths, + cssUrl, + watch + ); + const warnings = result.warnings().map((w) => w.toString()); + + const { code, warnings: esBuildWarnings } = await esbuild.transform( + result.css, + { + loader: 'css', + minify: true, + sourcefile: filePath, + } + ); + + if (esBuildWarnings.length > 0) { + warnings.push( + ...(await esbuild.formatMessages(esBuildWarnings, { kind: 'warning' })) + ); + } + + // Add to cache + await cacache.put( + cachePath, + key, + JSON.stringify({ + css: code, + warnings, + }) + ); + + return { + css: code, + warnings, + }; +} + +async function renderCss( + filePath: string, + css: string, + basePath: string, + styleIncludePaths?: string[] +): Promise { + const ext = path.extname(filePath); + + switch (ext) { + case '.sass': + case '.scss': { + /* + * Please be aware of the few differences in behaviour https://github.com/sass/dart-sass/blob/master/README.md#behavioral-differences-from-ruby-sass + * By default `npm install` will install sass. + * To use node-sass you need to use: + * Npm: + * `npm install node-sass --save-dev` + * Yarn: + * `yarn add node-sass --dev` + */ + let sassCompiler: any | undefined; + try { + sassCompiler = require('node-sass'); // Check if node-sass is explicitly included. + } catch { + sassCompiler = await import('sass'); + } + + return sassCompiler + .renderSync({ + file: filePath, + data: css, + indentedSyntax: '.sass' === ext, + importer: await import('node-sass-tilde-importer'), + includePaths: styleIncludePaths, + }) + .css.toString(); + } + case '.less': { + const { css: content } = await ( + await import('less') + ).render(css, { + filename: filePath, + javascriptEnabled: true, + paths: styleIncludePaths, + }); + + return content; + } + case '.styl': + case '.stylus': { + const stylus = await import('stylus'); + + return ( + stylus(css) + // add paths for resolve + .set('paths', [basePath, '.', ...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 optimizeCss( + filePath: string, + css: string, + browsers: string[], + basePath: string, + includePaths?: string[], + cssUrl?: CssUrl, + watch?: boolean +): LazyResult { + const postCssPlugins = []; + + if (cssUrl !== CssUrl.none) { + postCssPlugins.push(postcssUrl({ url: cssUrl })); + } + + postCssPlugins.push( + ...getTailwindPostCssPluginsIfPresent(basePath, includePaths, watch) + ); + + postCssPlugins.push( + postcssPresetEnv({ + browsers, + autoprefixer: true, + stage: 3, + }) + ); + + return postcss(postCssPlugins).process(css, { + from: filePath, + to: filePath.replace(path.extname(filePath), '.css'), + }); +} + +function generateKey( + content: string, + browserslistData: string[], + filePath: string +): string { + return createHash('sha1') + .update(ngPackagrVersion) + .update(content) + .update(browserslistData.join('')) + .update(filePath) + .digest('hex'); +} + +async function readCacheEntry( + cachePath: string, + key: string +): Promise { + const entry = await cacache.get.info(cachePath, key); + if (entry) { + return JSON.parse(await readFile(entry.path, 'utf8')); + } + + return undefined; +} + +parentPort.on('message', async ({ signal, port, workerOptions }) => { + try { + const result = await processCss(workerOptions); + port.postMessage({ ...result }); + } catch (error) { + port.postMessage({ error: error.message }); + } finally { + // Change the value of signal[0] to 1 + Atomics.add(signal, 0, 1); + // Unlock the main thread + Atomics.notify(signal, 0); + port.close(); + } +}); diff --git a/packages/angular/src/executors/package/ng-packagr-adjustments/styles/stylesheet-processor.di.ts b/packages/angular/src/executors/package/ng-packagr-adjustments/styles/stylesheet-processor.di.ts new file mode 100644 index 0000000000..868e90db24 --- /dev/null +++ b/packages/angular/src/executors/package/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/package/ng-packagr-adjustments/styles/stylesheet-processor.ts b/packages/angular/src/executors/package/ng-packagr-adjustments/styles/stylesheet-processor.ts new file mode 100644 index 0000000000..9a4abad18a --- /dev/null +++ b/packages/angular/src/executors/package/ng-packagr-adjustments/styles/stylesheet-processor.ts @@ -0,0 +1,102 @@ +/** + * Adapted from the original ng-packagr source. + * + * Changes made: + * - Added watch mode parameter. + */ + +import * as browserslist from 'browserslist'; +import * as findCacheDirectory from 'find-cache-dir'; +import { EsbuildExecutor } from 'ng-packagr/lib/esbuild/esbuild-executor'; +import * as log from 'ng-packagr/lib/utils/log'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import { MessageChannel, receiveMessageOnPort, Worker } from 'worker_threads'; + +export enum CssUrl { + inline = 'inline', + none = 'none', +} +export interface WorkerOptions { + filePath: string; + basePath: string; + browserslistData: string[]; + cssUrl?: CssUrl; + styleIncludePaths?: string[]; + cachePath: string; + alwaysUseWasm: boolean; + watch?: boolean; +} + +export interface WorkerResult { + css: string; + warnings: string[]; + error?: string; +} + +export class StylesheetProcessor { + private browserslistData: string[] | undefined; + private worker: Worker | undefined; + private readonly cachePath: string; + private alwaysUseWasm = !EsbuildExecutor.hasNativeSupport(); + + constructor( + private readonly basePath: string, + private readonly cssUrl?: CssUrl, + private readonly styleIncludePaths?: string[], + private readonly watch?: boolean + ) { + this.cachePath = + findCacheDirectory({ name: 'ng-packagr-styles' }) || tmpdir(); + } + + process(filePath: string) { + if (!this.worker) { + this.worker = new Worker( + join(__dirname, './stylesheet-processor-worker.js') + ); + } + + if (!this.browserslistData) { + this.browserslistData = browserslist(undefined, { path: this.basePath }); + } + + const workerOptions: WorkerOptions = { + filePath, + basePath: this.basePath, + cssUrl: this.cssUrl, + styleIncludePaths: this.styleIncludePaths, + browserslistData: this.browserslistData, + cachePath: this.cachePath, + alwaysUseWasm: this.alwaysUseWasm, + watch: this.watch, + }; + + const ioChannel = new MessageChannel(); + + try { + const signal = new Int32Array(new SharedArrayBuffer(4)); + this.worker.postMessage( + { signal, port: ioChannel.port1, workerOptions }, + [ioChannel.port1] + ); + + // Sleep until signal[0] is 0 + Atomics.wait(signal, 0, 0); + + const { css, warnings, error } = receiveMessageOnPort( + ioChannel.port2 + ).message; + if (error) { + throw new Error(error); + } + + warnings.forEach((msg) => log.warn(msg)); + return css; + } finally { + ioChannel.port1.close(); + ioChannel.port2.close(); + this.worker.unref(); + } + } +} diff --git a/packages/angular/src/executors/package/ng-packagr-adjustments/utilities/tailwindcss.ts b/packages/angular/src/executors/package/ng-packagr-adjustments/utilities/tailwindcss.ts new file mode 100644 index 0000000000..3108b8ddea --- /dev/null +++ b/packages/angular/src/executors/package/ng-packagr-adjustments/utilities/tailwindcss.ts @@ -0,0 +1,60 @@ +import { logger } from '@nrwl/devkit'; +import { appRootPath } from '@nrwl/tao/src/utils/app-root'; +import { existsSync } from 'fs'; +import { join, relative } from 'path'; +import * as postcssImport from 'postcss-import'; + +export function getTailwindPostCssPluginsIfPresent( + basePath: string, + includePaths?: string[], + watch?: boolean +) { + // 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; + } + } + + // Only load Tailwind CSS plugin if configuration file was found. + if (!tailwindConfigPath) { + return []; + } + + let tailwindPackagePath: string | undefined; + try { + tailwindPackagePath = require.resolve('tailwindcss'); + } catch { + const relativeTailwindConfigPath = relative( + appRootPath, + tailwindConfigPath + ); + logger.warn( + `Tailwind CSS configuration file found (${relativeTailwindConfigPath})` + + ` but the 'tailwindcss' package is not installed.` + + ` To enable Tailwind CSS, please install the 'tailwindcss' package.` + ); + + return []; + } + + if (!tailwindPackagePath) { + return []; + } + + if (process.env['TAILWIND_MODE'] === undefined) { + process.env['TAILWIND_MODE'] = watch ? 'watch' : 'build'; + } + + return [ + postcssImport({ + addModulesDirectories: includePaths ?? [], + resolve: (url: string) => (url.startsWith('~') ? url.substr(1) : url), + }), + require(tailwindPackagePath)({ config: tailwindConfigPath }), + ]; +} diff --git a/packages/angular/src/executors/package/package.impl.spec.ts b/packages/angular/src/executors/package/package.impl.spec.ts index 7b1f24c53a..ce962f3be2 100644 --- a/packages/angular/src/executors/package/package.impl.spec.ts +++ b/packages/angular/src/executors/package/package.impl.spec.ts @@ -16,6 +16,7 @@ describe('Package executor', () => { let ngPackagrBuildMock: jest.Mock; let ngPackagerWatchSubject: BehaviorSubject; let ngPackagrWatchMock: jest.Mock; + let ngPackagrWithBuildTransformMock: jest.Mock; let ngPackagrWithTsConfigMock: jest.Mock; let options: BuildAngularLibraryExecutorOptions; let tsConfig: { options: { paths: { [key: string]: string[] } } }; @@ -36,11 +37,13 @@ describe('Package executor', () => { ngPackagrBuildMock = jest.fn(() => Promise.resolve()); ngPackagerWatchSubject = new BehaviorSubject(undefined); ngPackagrWatchMock = jest.fn(() => ngPackagerWatchSubject.asObservable()); + ngPackagrWithBuildTransformMock = jest.fn(); ngPackagrWithTsConfigMock = jest.fn(); - (ngPackagr.ngPackagr as jest.Mock).mockImplementation(() => ({ + (ngPackagr.NgPackagr as jest.Mock).mockImplementation(() => ({ build: ngPackagrBuildMock, forProject: jest.fn(), watch: ngPackagrWatchMock, + withBuildTransform: ngPackagrWithBuildTransformMock, withTsConfig: ngPackagrWithTsConfigMock, })); diff --git a/packages/angular/src/executors/package/package.impl.ts b/packages/angular/src/executors/package/package.impl.ts index 7584c60dc6..91a5d878c2 100644 --- a/packages/angular/src/executors/package/package.impl.ts +++ b/packages/angular/src/executors/package/package.impl.ts @@ -13,6 +13,11 @@ import { resolve } from 'path'; 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 { + NX_PACKAGE_PROVIDERS, + NX_PACKAGE_TRANSFORM, +} from './ng-packagr-adjustments/ng-package/package.di'; import type { BuildAngularLibraryExecutorOptions } from './schema'; async function initializeNgPackagr( @@ -20,8 +25,13 @@ async function initializeNgPackagr( context: ExecutorContext, projectDependencies: DependentBuildableProjectNode[] ): Promise { - const packager = (await import('ng-packagr')).ngPackagr(); + const packager = new (await import('ng-packagr')).NgPackagr([ + ...NX_PACKAGE_PROVIDERS, + ...NX_ENTRY_POINT_PROVIDERS, + ]); + packager.forProject(resolve(context.root, options.project)); + packager.withBuildTransform(NX_PACKAGE_TRANSFORM.provide); if (options.tsConfig) { // read the tsconfig and modify its path in memory to diff --git a/packages/angular/src/generators/library/library.spec.ts b/packages/angular/src/generators/library/library.spec.ts index d71b13a373..c37d2e7522 100644 --- a/packages/angular/src/generators/library/library.spec.ts +++ b/packages/angular/src/generators/library/library.spec.ts @@ -69,6 +69,7 @@ describe('lib', () => { // ASSERT const packageJson = readJson(appTree, '/package.json'); expect(packageJson.devDependencies['ng-packagr']).toBeUndefined(); + expect(packageJson.devDependencies['postcss-import']).toBeUndefined(); }); it('should update package.json when publishable', async () => { @@ -81,6 +82,7 @@ describe('lib', () => { // ASSERT const packageJson = readJson(appTree, '/package.json'); expect(packageJson.devDependencies['ng-packagr']).toBeDefined(); + expect(packageJson.devDependencies['postcss-import']).toBeDefined(); }); it('should update tsconfig.lib.prod.json when enableIvy', async () => { diff --git a/packages/angular/src/generators/library/library.ts b/packages/angular/src/generators/library/library.ts index e2a0081ff3..a76f1da661 100644 --- a/packages/angular/src/generators/library/library.ts +++ b/packages/angular/src/generators/library/library.ts @@ -1,6 +1,6 @@ import { + addDependenciesToPackageJson, formatFiles, - getWorkspaceLayout, installPackagesTask, moveFilesToNewDirectory, Tree, @@ -70,6 +70,10 @@ export async function libraryGenerator(host: Tree, schema: Partial) { setStrictMode(host, options); await addLinting(host, options); + if (options.publishable) { + addDependenciesToPackageJson(host, {}, { 'postcss-import': '^14.0.2' }); + } + if (options.standaloneConfig) { await convertToNxProjectGenerator(host, { project: options.name, diff --git a/packages/angular/src/migrations/update-13-0-0/add-postcss-import.spec.ts b/packages/angular/src/migrations/update-13-0-0/add-postcss-import.spec.ts new file mode 100644 index 0000000000..3b552eeb39 --- /dev/null +++ b/packages/angular/src/migrations/update-13-0-0/add-postcss-import.spec.ts @@ -0,0 +1,30 @@ +import { readJson, Tree, updateJson } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import addPostCssImport from './add-postcss-import'; + +describe('add-postcss-import migration', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(2); + }); + + it('should not add postcss-import when ng-packagr is not installed', () => { + addPostCssImport(tree); + + const packageJson = readJson(tree, 'package.json'); + expect(packageJson.devDependencies['postcss-import']).toBeUndefined(); + }); + + it('should add postcss-import when ng-packagr is installed', () => { + updateJson(tree, 'package.json', (json) => { + json.devDependencies['ng-packagr'] = '~12.2.3'; + return json; + }); + + addPostCssImport(tree); + + const packageJson = readJson(tree, 'package.json'); + expect(packageJson.devDependencies['postcss-import']).toBeDefined(); + }); +}); diff --git a/packages/angular/src/migrations/update-13-0-0/add-postcss-import.ts b/packages/angular/src/migrations/update-13-0-0/add-postcss-import.ts new file mode 100644 index 0000000000..83d4589e66 --- /dev/null +++ b/packages/angular/src/migrations/update-13-0-0/add-postcss-import.ts @@ -0,0 +1,25 @@ +import { + addDependenciesToPackageJson, + formatFiles, + readJson, + Tree, +} from '@nrwl/devkit'; + +export default async function (tree: Tree) { + const { devDependencies } = readJson(tree, 'package.json'); + + // Don't add if ng-packagr is not installed + if (!devDependencies['ng-packagr']) { + return; + } + + const task = addDependenciesToPackageJson( + tree, + {}, + { 'postcss-import': '^14.0.2' } + ); + + await formatFiles(tree); + + return task; +} diff --git a/scripts/depcheck/missing.ts b/scripts/depcheck/missing.ts index ad23cf1fea..3d4481a690 100644 --- a/scripts/depcheck/missing.ts +++ b/scripts/depcheck/missing.ts @@ -15,10 +15,26 @@ const IGNORE_MATCHES = { '@ngrx/router-store', '@ngrx/store', '@storybook/angular', - 'injection-js', - 'ng-packagr', 'rxjs', 'semver', + // installed dynamically by the library generator + 'ng-packagr', + // ng-packagr deps, some are handled if not installed + 'injection-js', + 'browserslist', + 'cacache', + 'find-cache-dir', + 'less', + 'node-sass', + 'node-sass-tilde-importer', + 'ora', + 'postcss', + 'postcss-import', + 'postcss-preset-env', + 'postcss-url', + 'sass', + 'stylus', + 'tailwindcss', ], cli: ['@nrwl/cli'], cypress: ['cypress', '@angular-devkit/schematics', '@nrwl/cypress'], diff --git a/yarn.lock b/yarn.lock index da37894084..38d1760f29 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11813,6 +11813,11 @@ es6-weak-map@^2.0.3: es6-iterator "^2.0.3" es6-symbol "^3.1.1" +esbuild-wasm@^0.12.15: + version "0.12.29" + resolved "https://registry.yarnpkg.com/esbuild-wasm/-/esbuild-wasm-0.12.29.tgz#1d210bb7d463b2ca51c54d69bb4192d9709f6100" + integrity sha512-amSuB/qOGnTFYLOxGHDGosQbOKZnrinniPHFf6ZxzeNH7WAjLkjXluKyKAtX2YuhTkUXm9XV9igl13iqYZ44fQ== + esbuild@0.12.17: version "0.12.17" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.12.17.tgz#5816f905c2905de0ebbc658860df7b5b48afbcd3" @@ -18358,10 +18363,10 @@ next@^11.1.2: "@next/swc-linux-x64-gnu" "11.1.2" "@next/swc-win32-x64-msvc" "11.1.2" -ng-packagr@~12.2.0: - version "12.2.0" - resolved "https://registry.yarnpkg.com/ng-packagr/-/ng-packagr-12.2.0.tgz#53fe47391b5ddaf5f2c24eaecb23d8a10235d887" - integrity sha512-M/qq78Gb4q13t6SFX70W2DrPxyooSkLwXzhWozjD8yWGihx4q+54a72ODGx7jIrB4fQgrGDcMUTM7t1zGYir8Q== +ng-packagr@~12.2.3: + version "12.2.3" + resolved "https://registry.yarnpkg.com/ng-packagr/-/ng-packagr-12.2.3.tgz#1597f8e70a78ab88bb395675358942bfc14cbc07" + integrity sha512-2KHoglc7UgJMnzkytzZ1wU+IEkb6UrxoU4QZxnF5BSFh9vjUra2nCXH+EKkvxD3WTj0ikXmNIMeNWbwoZpGkgA== dependencies: "@rollup/plugin-commonjs" "^20.0.0" "@rollup/plugin-json" "^4.1.0" @@ -18373,7 +18378,7 @@ ng-packagr@~12.2.0: chokidar "^3.5.1" commander "^8.0.0" dependency-graph "^0.11.0" - esbuild "^0.12.15" + esbuild-wasm "^0.12.15" find-cache-dir "^3.3.1" glob "^7.1.6" injection-js "^2.4.0" @@ -18389,6 +18394,8 @@ ng-packagr@~12.2.0: rxjs "^6.5.0" sass "^1.32.8" stylus "^0.54.8" + optionalDependencies: + esbuild "^0.12.15" ngrx-store-freeze@0.2.4: version "0.2.4"