diff --git a/e2e/angular-extensions/src/tailwind.test.ts b/e2e/angular-extensions/src/tailwind.test.ts index 9ab473d82c..a8140e84e4 100644 --- a/e2e/angular-extensions/src/tailwind.test.ts +++ b/e2e/angular-extensions/src/tailwind.test.ts @@ -12,390 +12,387 @@ import { } from '@nrwl/e2e/utils'; describe('Tailwind support', () => { - it('tests are disabled', () => { - expect(1).toEqual(1); + let project: string; + + const defaultButtonBgColor = 'bg-blue-700'; + + const buildLibWithTailwind = { + name: uniq('build-lib-with-tailwind'), + buttonBgColor: 'bg-green-800', + }; + const pubLibWithTailwind = { + name: uniq('pub-lib-with-tailwind'), + buttonBgColor: 'bg-red-900', + }; + + const spacing = { + root: { + sm: '2px', + md: '4px', + lg: '8px', + }, + projectVariant1: { + sm: '1px', + md: '2px', + lg: '4px', + }, + projectVariant2: { + sm: '4px', + md: '8px', + lg: '16px', + }, + projectVariant3: { + sm: '8px', + md: '16px', + lg: '32px', + }, + }; + + const createWorkspaceTailwindConfigFile = () => { + const tailwindConfigFile = 'tailwind.config.js'; + + const tailwindConfig = `module.exports = { + mode: 'jit', + purge: ['./apps/**/*.{html,ts}', './libs/**/*.{html,ts}'], + darkMode: false, + theme: { + spacing: { + sm: '${spacing.root.sm}', + md: '${spacing.root.md}', + lg: '${spacing.root.lg}', + }, + }, + variants: { extend: {} }, + plugins: [], + }; + `; + + updateFile(tailwindConfigFile, tailwindConfig); + }; + + const createTailwindConfigFile = ( + tailwindConfigFile = 'tailwind.config.js', + libSpacing: typeof spacing['projectVariant1'] + ) => { + const tailwindConfig = `const { createGlobPatternsForDependencies } = require('@nrwl/angular/tailwind'); + const { join } = require('path'); + + module.exports = { + mode: 'jit', + purge: [ + join(__dirname, 'src/**/*.{html,ts}'), + ...createGlobPatternsForDependencies(__dirname), + ], + darkMode: false, + theme: { + spacing: { + sm: '${libSpacing.sm}', + md: '${libSpacing.md}', + lg: '${libSpacing.lg}', + }, + }, + variants: { extend: {} }, + plugins: [], + }; + `; + + updateFile(tailwindConfigFile, tailwindConfig); + }; + + const updateTailwindConfig = ( + tailwindConfigPath: string, + projectSpacing: typeof spacing['root'] + ) => { + const tailwindConfig = readFile(tailwindConfigPath); + + const tailwindConfigUpdated = tailwindConfig.replace( + 'theme: {', + `theme: { + spacing: { + sm: '${projectSpacing.sm}', + md: '${projectSpacing.md}', + lg: '${projectSpacing.lg}', + },` + ); + + updateFile(tailwindConfigPath, tailwindConfigUpdated); + }; + + beforeAll(() => { + project = newProject(); + + // Create tailwind config in the workspace root + createWorkspaceTailwindConfigFile(); + }); + + afterAll(() => cleanupProject()); + + describe('Libraries', () => { + const createLibComponent = ( + lib: string, + buttonBgColor: string = defaultButtonBgColor + ) => { + updateFile( + `libs/${lib}/src/lib/foo.component.ts`, + `import { Component } from '@angular/core'; + + @Component({ + selector: '${project}-foo', + template: '', + styles: [\` + .custom-btn { + @apply m-md p-sm; + } + \`] + }) + export class FooComponent {} + ` + ); + + updateFile( + `libs/${lib}/src/lib/${lib}.module.ts`, + `import { NgModule } from '@angular/core'; + import { CommonModule } from '@angular/common'; + import { FooComponent } from './foo.component'; + + @NgModule({ + imports: [CommonModule], + declarations: [FooComponent], + exports: [FooComponent], + }) + export class LibModule {} + ` + ); + + updateFile( + `libs/${lib}/src/index.ts`, + `export * from './lib/foo.component'; + export * from './lib/${lib}.module'; + ` + ); + }; + + const assertLibComponentStyles = ( + lib: string, + libSpacing: typeof spacing['root'] + ) => { + const builtComponentContent = readFile( + `dist/libs/${lib}/esm2020/lib/foo.component.mjs` + ); + let expectedStylesRegex = new RegExp( + `styles: \\[\\"\\.custom\\-btn(\\[_ngcontent\\-%COMP%\\])?{margin:${libSpacing.md};padding:${libSpacing.sm}}(\\\\n)?\\"\\]` + ); + + expect(builtComponentContent).toMatch(expectedStylesRegex); + }; + + it('should generate a buildable library with tailwind and build correctly', () => { + runCLI( + `generate @nrwl/angular:lib ${buildLibWithTailwind.name} --buildable --add-tailwind --no-interactive` + ); + updateTailwindConfig( + `libs/${buildLibWithTailwind.name}/tailwind.config.js`, + spacing.projectVariant1 + ); + createLibComponent( + buildLibWithTailwind.name, + buildLibWithTailwind.buttonBgColor + ); + + runCLI(`build ${buildLibWithTailwind.name}`); + + assertLibComponentStyles( + buildLibWithTailwind.name, + spacing.projectVariant1 + ); + }); + + it('should set up tailwind in a previously generated buildable library and build correctly', () => { + const buildLibSetupTailwind = uniq('build-lib-setup-tailwind'); + runCLI( + `generate @nrwl/angular:lib ${buildLibSetupTailwind} --buildable --no-interactive` + ); + runCLI( + `generate @nrwl/angular:setup-tailwind ${buildLibSetupTailwind} --no-interactive` + ); + updateTailwindConfig( + `libs/${buildLibSetupTailwind}/tailwind.config.js`, + spacing.projectVariant2 + ); + createLibComponent(buildLibSetupTailwind); + + runCLI(`build ${buildLibSetupTailwind}`); + + assertLibComponentStyles(buildLibSetupTailwind, spacing.projectVariant2); + }); + + it('should correctly build a buildable library with a tailwind.config.js file in the project root or workspace root', () => { + const buildLibNoProjectConfig = uniq('build-lib-no-project-config'); + runCLI( + `generate @nrwl/angular:lib ${buildLibNoProjectConfig} --buildable --no-interactive` + ); + createTailwindConfigFile( + `libs/${buildLibNoProjectConfig}/tailwind.config.js`, + spacing.projectVariant3 + ); + createLibComponent(buildLibNoProjectConfig); + + runCLI(`build ${buildLibNoProjectConfig}`); + + assertLibComponentStyles( + buildLibNoProjectConfig, + spacing.projectVariant3 + ); + + // remove tailwind.config.js file from the project root to test the one in the workspace root + removeFile(`libs/${buildLibNoProjectConfig}/tailwind.config.js`); + + runCLI(`build ${buildLibNoProjectConfig}`); + + assertLibComponentStyles(buildLibNoProjectConfig, spacing.root); + }); + + it('should generate a publishable library with tailwind and build correctly', () => { + runCLI( + `generate @nrwl/angular:lib ${pubLibWithTailwind.name} --publishable --add-tailwind --importPath=@${project}/${pubLibWithTailwind.name} --no-interactive` + ); + updateTailwindConfig( + `libs/${pubLibWithTailwind.name}/tailwind.config.js`, + spacing.projectVariant1 + ); + createLibComponent( + pubLibWithTailwind.name, + pubLibWithTailwind.buttonBgColor + ); + + runCLI(`build ${pubLibWithTailwind.name}`); + + assertLibComponentStyles( + pubLibWithTailwind.name, + spacing.projectVariant1 + ); + }); + + it('should set up tailwind in a previously generated publishable library and build correctly', () => { + const pubLibSetupTailwind = uniq('pub-lib-setup-tailwind'); + runCLI( + `generate @nrwl/angular:lib ${pubLibSetupTailwind} --publishable --importPath=@${project}/${pubLibSetupTailwind} --no-interactive` + ); + runCLI( + `generate @nrwl/angular:setup-tailwind ${pubLibSetupTailwind} --no-interactive` + ); + updateTailwindConfig( + `libs/${pubLibSetupTailwind}/tailwind.config.js`, + spacing.projectVariant2 + ); + createLibComponent(pubLibSetupTailwind); + + runCLI(`build ${pubLibSetupTailwind}`); + + assertLibComponentStyles(pubLibSetupTailwind, spacing.projectVariant2); + }); + + it('should correctly build a publishable library with a tailwind.config.js file in the project root or workspace root', () => { + const pubLibNoProjectConfig = uniq('pub-lib-no-project-config'); + runCLI( + `generate @nrwl/angular:lib ${pubLibNoProjectConfig} --publishable --importPath=@${project}/${pubLibNoProjectConfig} --no-interactive` + ); + createTailwindConfigFile( + `libs/${pubLibNoProjectConfig}/tailwind.config.js`, + spacing.projectVariant3 + ); + createLibComponent(pubLibNoProjectConfig); + + runCLI(`build ${pubLibNoProjectConfig}`); + + assertLibComponentStyles(pubLibNoProjectConfig, spacing.projectVariant3); + + // remove tailwind.config.js file from the project root to test the one in the workspace root + removeFile(`libs/${pubLibNoProjectConfig}/tailwind.config.js`); + + runCLI(`build ${pubLibNoProjectConfig}`); + + assertLibComponentStyles(pubLibNoProjectConfig, spacing.root); + }); + }); + + describe('Applications', () => { + const updateAppComponent = (app: string) => { + updateFile( + `apps/${app}/src/app/app.component.html`, + `` + ); + + updateFile( + `apps/${app}/src/app/app.component.css`, + `.custom-btn { + @apply m-md p-sm; + }` + ); + }; + + const readAppStylesBundle = (app: string) => { + const stylesBundlePath = listFiles(`dist/apps/${app}`).find((file) => + file.startsWith('styles.') + ); + const stylesBundle = readFile(`dist/apps/${app}/${stylesBundlePath}`); + + return stylesBundle; + }; + + const assertAppComponentStyles = ( + app: string, + appSpacing: typeof spacing['root'] + ) => { + const mainBundlePath = listFiles(`dist/apps/${app}`).find((file) => + file.startsWith('main.') + ); + const mainBundle = readFile(`dist/apps/${app}/${mainBundlePath}`); + let expectedStylesRegex = new RegExp( + `styles:\\[\\"\\.custom\\-btn\\[_ngcontent\\-%COMP%\\]{margin:${appSpacing.md};padding:${appSpacing.sm}}\\"\\]` + ); + + expect(mainBundle).toMatch(expectedStylesRegex); + }; + + it('should build correctly and only output the tailwind utilities used', () => { + const appWithTailwind = uniq('app-with-tailwind'); + runCLI( + `generate @nrwl/angular:app ${appWithTailwind} --add-tailwind --no-interactive` + ); + updateTailwindConfig( + `apps/${appWithTailwind}/tailwind.config.js`, + spacing.projectVariant1 + ); + updateFile( + `apps/${appWithTailwind}/src/app/app.module.ts`, + `import { NgModule } from '@angular/core'; + import { BrowserModule } from '@angular/platform-browser'; + import { LibModule as LibModule1 } from '@${project}/${buildLibWithTailwind.name}'; + import { LibModule as LibModule2 } from '@${project}/${pubLibWithTailwind.name}'; + + import { AppComponent } from './app.component'; + + @NgModule({ + declarations: [AppComponent], + imports: [BrowserModule, LibModule1, LibModule2], + providers: [], + bootstrap: [AppComponent], + }) + export class AppModule {} + ` + ); + updateAppComponent(appWithTailwind); + + runCLI(`build ${appWithTailwind}`); + + assertAppComponentStyles(appWithTailwind, spacing.projectVariant1); + let stylesBundle = readAppStylesBundle(appWithTailwind); + expect(stylesBundle).toContain('.text-white'); + expect(stylesBundle).not.toContain('.text-black'); + expect(stylesBundle).toContain(`.${buildLibWithTailwind.buttonBgColor}`); + expect(stylesBundle).toContain(`.${pubLibWithTailwind.buttonBgColor}`); + expect(stylesBundle).not.toContain(`.${defaultButtonBgColor}`); + }); }); - // let project: string; - // - // const defaultButtonBgColor = 'bg-blue-700'; - // - // const buildLibWithTailwind = { - // name: uniq('build-lib-with-tailwind'), - // buttonBgColor: 'bg-green-800', - // }; - // const pubLibWithTailwind = { - // name: uniq('pub-lib-with-tailwind'), - // buttonBgColor: 'bg-red-900', - // }; - // - // const spacing = { - // root: { - // sm: '2px', - // md: '4px', - // lg: '8px', - // }, - // projectVariant1: { - // sm: '1px', - // md: '2px', - // lg: '4px', - // }, - // projectVariant2: { - // sm: '4px', - // md: '8px', - // lg: '16px', - // }, - // projectVariant3: { - // sm: '8px', - // md: '16px', - // lg: '32px', - // }, - // }; - // - // const createWorkspaceTailwindConfigFile = () => { - // const tailwindConfigFile = 'tailwind.config.js'; - // - // const tailwindConfig = `module.exports = { - // mode: 'jit', - // purge: ['./apps/**/*.{html,ts}', './libs/**/*.{html,ts}'], - // darkMode: false, - // theme: { - // spacing: { - // sm: '${spacing.root.sm}', - // md: '${spacing.root.md}', - // lg: '${spacing.root.lg}', - // }, - // }, - // variants: { extend: {} }, - // plugins: [], - // }; - // `; - // - // updateFile(tailwindConfigFile, tailwindConfig); - // }; - // - // const createTailwindConfigFile = ( - // tailwindConfigFile = 'tailwind.config.js', - // libSpacing: typeof spacing['projectVariant1'] - // ) => { - // const tailwindConfig = `const { createGlobPatternsForDependencies } = require('@nrwl/angular/tailwind'); - // const { join } = require('path'); - // - // module.exports = { - // mode: 'jit', - // purge: [ - // join(__dirname, 'src/**/*.{html,ts}'), - // ...createGlobPatternsForDependencies(__dirname), - // ], - // darkMode: false, - // theme: { - // spacing: { - // sm: '${libSpacing.sm}', - // md: '${libSpacing.md}', - // lg: '${libSpacing.lg}', - // }, - // }, - // variants: { extend: {} }, - // plugins: [], - // }; - // `; - // - // updateFile(tailwindConfigFile, tailwindConfig); - // }; - // - // const updateTailwindConfig = ( - // tailwindConfigPath: string, - // projectSpacing: typeof spacing['root'] - // ) => { - // const tailwindConfig = readFile(tailwindConfigPath); - // - // const tailwindConfigUpdated = tailwindConfig.replace( - // 'theme: {', - // `theme: { - // spacing: { - // sm: '${projectSpacing.sm}', - // md: '${projectSpacing.md}', - // lg: '${projectSpacing.lg}', - // },` - // ); - // - // updateFile(tailwindConfigPath, tailwindConfigUpdated); - // }; - // - // beforeAll(() => { - // project = newProject(); - // - // // Create tailwind config in the workspace root - // createWorkspaceTailwindConfigFile(); - // }); - // - // afterAll(() => cleanupProject()); - // - // describe('Libraries', () => { - // const createLibComponent = ( - // lib: string, - // buttonBgColor: string = defaultButtonBgColor - // ) => { - // updateFile( - // `libs/${lib}/src/lib/foo.component.ts`, - // `import { Component } from '@angular/core'; - // - // @Component({ - // selector: '${project}-foo', - // template: '', - // styles: [\` - // .custom-btn { - // @apply m-md p-sm; - // } - // \`] - // }) - // export class FooComponent {} - // ` - // ); - // - // updateFile( - // `libs/${lib}/src/lib/${lib}.module.ts`, - // `import { NgModule } from '@angular/core'; - // import { CommonModule } from '@angular/common'; - // import { FooComponent } from './foo.component'; - // - // @NgModule({ - // imports: [CommonModule], - // declarations: [FooComponent], - // exports: [FooComponent], - // }) - // export class LibModule {} - // ` - // ); - // - // updateFile( - // `libs/${lib}/src/index.ts`, - // `export * from './lib/foo.component'; - // export * from './lib/${lib}.module'; - // ` - // ); - // }; - // - // const assertLibComponentStyles = ( - // lib: string, - // libSpacing: typeof spacing['root'] - // ) => { - // const builtComponentContent = readFile( - // `dist/libs/${lib}/esm2020/lib/foo.component.mjs` - // ); - // let expectedStylesRegex = new RegExp( - // `styles: \\[\\"\\.custom\\-btn(\\[_ngcontent\\-%COMP%\\])?{margin:${libSpacing.md};padding:${libSpacing.sm}}(\\\\n)?\\"\\]` - // ); - // - // expect(builtComponentContent).toMatch(expectedStylesRegex); - // }; - // - // it('should generate a buildable library with tailwind and build correctly', () => { - // runCLI( - // `generate @nrwl/angular:lib ${buildLibWithTailwind.name} --buildable --add-tailwind --no-interactive` - // ); - // updateTailwindConfig( - // `libs/${buildLibWithTailwind.name}/tailwind.config.js`, - // spacing.projectVariant1 - // ); - // createLibComponent( - // buildLibWithTailwind.name, - // buildLibWithTailwind.buttonBgColor - // ); - // - // runCLI(`build ${buildLibWithTailwind.name}`); - // - // assertLibComponentStyles( - // buildLibWithTailwind.name, - // spacing.projectVariant1 - // ); - // }); - // - // it('should set up tailwind in a previously generated buildable library and build correctly', () => { - // const buildLibSetupTailwind = uniq('build-lib-setup-tailwind'); - // runCLI( - // `generate @nrwl/angular:lib ${buildLibSetupTailwind} --buildable --no-interactive` - // ); - // runCLI( - // `generate @nrwl/angular:setup-tailwind ${buildLibSetupTailwind} --no-interactive` - // ); - // updateTailwindConfig( - // `libs/${buildLibSetupTailwind}/tailwind.config.js`, - // spacing.projectVariant2 - // ); - // createLibComponent(buildLibSetupTailwind); - // - // runCLI(`build ${buildLibSetupTailwind}`); - // - // assertLibComponentStyles(buildLibSetupTailwind, spacing.projectVariant2); - // }); - // - // it('should correctly build a buildable library with a tailwind.config.js file in the project root or workspace root', () => { - // const buildLibNoProjectConfig = uniq('build-lib-no-project-config'); - // runCLI( - // `generate @nrwl/angular:lib ${buildLibNoProjectConfig} --buildable --no-interactive` - // ); - // createTailwindConfigFile( - // `libs/${buildLibNoProjectConfig}/tailwind.config.js`, - // spacing.projectVariant3 - // ); - // createLibComponent(buildLibNoProjectConfig); - // - // runCLI(`build ${buildLibNoProjectConfig}`); - // - // assertLibComponentStyles( - // buildLibNoProjectConfig, - // spacing.projectVariant3 - // ); - // - // // remove tailwind.config.js file from the project root to test the one in the workspace root - // removeFile(`libs/${buildLibNoProjectConfig}/tailwind.config.js`); - // - // runCLI(`build ${buildLibNoProjectConfig}`); - // - // assertLibComponentStyles(buildLibNoProjectConfig, spacing.root); - // }); - // - // it('should generate a publishable library with tailwind and build correctly', () => { - // runCLI( - // `generate @nrwl/angular:lib ${pubLibWithTailwind.name} --publishable --add-tailwind --importPath=@${project}/${pubLibWithTailwind.name} --no-interactive` - // ); - // updateTailwindConfig( - // `libs/${pubLibWithTailwind.name}/tailwind.config.js`, - // spacing.projectVariant1 - // ); - // createLibComponent( - // pubLibWithTailwind.name, - // pubLibWithTailwind.buttonBgColor - // ); - // - // runCLI(`build ${pubLibWithTailwind.name}`); - // - // assertLibComponentStyles( - // pubLibWithTailwind.name, - // spacing.projectVariant1 - // ); - // }); - // - // it('should set up tailwind in a previously generated publishable library and build correctly', () => { - // const pubLibSetupTailwind = uniq('pub-lib-setup-tailwind'); - // runCLI( - // `generate @nrwl/angular:lib ${pubLibSetupTailwind} --publishable --importPath=@${project}/${pubLibSetupTailwind} --no-interactive` - // ); - // runCLI( - // `generate @nrwl/angular:setup-tailwind ${pubLibSetupTailwind} --no-interactive` - // ); - // updateTailwindConfig( - // `libs/${pubLibSetupTailwind}/tailwind.config.js`, - // spacing.projectVariant2 - // ); - // createLibComponent(pubLibSetupTailwind); - // - // runCLI(`build ${pubLibSetupTailwind}`); - // - // assertLibComponentStyles(pubLibSetupTailwind, spacing.projectVariant2); - // }); - // - // it('should correctly build a publishable library with a tailwind.config.js file in the project root or workspace root', () => { - // const pubLibNoProjectConfig = uniq('pub-lib-no-project-config'); - // runCLI( - // `generate @nrwl/angular:lib ${pubLibNoProjectConfig} --publishable --importPath=@${project}/${pubLibNoProjectConfig} --no-interactive` - // ); - // createTailwindConfigFile( - // `libs/${pubLibNoProjectConfig}/tailwind.config.js`, - // spacing.projectVariant3 - // ); - // createLibComponent(pubLibNoProjectConfig); - // - // runCLI(`build ${pubLibNoProjectConfig}`); - // - // assertLibComponentStyles(pubLibNoProjectConfig, spacing.projectVariant3); - // - // // remove tailwind.config.js file from the project root to test the one in the workspace root - // removeFile(`libs/${pubLibNoProjectConfig}/tailwind.config.js`); - // - // runCLI(`build ${pubLibNoProjectConfig}`); - // - // assertLibComponentStyles(pubLibNoProjectConfig, spacing.root); - // }); - // }); - // - // describe('Applications', () => { - // const updateAppComponent = (app: string) => { - // updateFile( - // `apps/${app}/src/app/app.component.html`, - // `` - // ); - // - // updateFile( - // `apps/${app}/src/app/app.component.css`, - // `.custom-btn { - // @apply m-md p-sm; - // }` - // ); - // }; - // - // const readAppStylesBundle = (app: string) => { - // const stylesBundlePath = listFiles(`dist/apps/${app}`).find((file) => - // file.startsWith('styles.') - // ); - // const stylesBundle = readFile(`dist/apps/${app}/${stylesBundlePath}`); - // - // return stylesBundle; - // }; - // - // const assertAppComponentStyles = ( - // app: string, - // appSpacing: typeof spacing['root'] - // ) => { - // const mainBundlePath = listFiles(`dist/apps/${app}`).find((file) => - // file.startsWith('main.') - // ); - // const mainBundle = readFile(`dist/apps/${app}/${mainBundlePath}`); - // let expectedStylesRegex = new RegExp( - // `styles:\\[\\"\\.custom\\-btn\\[_ngcontent\\-%COMP%\\]{margin:${appSpacing.md};padding:${appSpacing.sm}}\\"\\]` - // ); - // - // expect(mainBundle).toMatch(expectedStylesRegex); - // }; - // - // it('should build correctly and only output the tailwind utilities used', () => { - // const appWithTailwind = uniq('app-with-tailwind'); - // runCLI( - // `generate @nrwl/angular:app ${appWithTailwind} --add-tailwind --no-interactive` - // ); - // updateTailwindConfig( - // `apps/${appWithTailwind}/tailwind.config.js`, - // spacing.projectVariant1 - // ); - // updateFile( - // `apps/${appWithTailwind}/src/app/app.module.ts`, - // `import { NgModule } from '@angular/core'; - // import { BrowserModule } from '@angular/platform-browser'; - // import { LibModule as LibModule1 } from '@${project}/${buildLibWithTailwind.name}'; - // import { LibModule as LibModule2 } from '@${project}/${pubLibWithTailwind.name}'; - // - // import { AppComponent } from './app.component'; - // - // @NgModule({ - // declarations: [AppComponent], - // imports: [BrowserModule, LibModule1, LibModule2], - // providers: [], - // bootstrap: [AppComponent], - // }) - // export class AppModule {} - // ` - // ); - // updateAppComponent(appWithTailwind); - // - // runCLI(`build ${appWithTailwind}`); - // - // assertAppComponentStyles(appWithTailwind, spacing.projectVariant1); - // let stylesBundle = readAppStylesBundle(appWithTailwind); - // expect(stylesBundle).toContain('.text-white'); - // expect(stylesBundle).not.toContain('.text-black'); - // expect(stylesBundle).toContain(`.${buildLibWithTailwind.buttonBgColor}`); - // expect(stylesBundle).toContain(`.${pubLibWithTailwind.buttonBgColor}`); - // expect(stylesBundle).not.toContain(`.${defaultButtonBgColor}`); - // }); - // }); }); 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 82dcb6c5da..2fd67e7422 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 @@ -58,7 +58,7 @@ export const nxCompileNgcTransformFactory = ( declaration: true, target: ts.ScriptTarget.ES2020, }, - entryPoint.cache.stylesheetProcessor, + entryPoint.cache.stylesheetProcessor as any, null, options.watch ); diff --git a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ngc/compile-source-files.ts b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ngc/compile-source-files.ts index 042c10b7c9..48bf448878 100644 --- a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ngc/compile-source-files.ts +++ b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ngc/compile-source-files.ts @@ -3,6 +3,7 @@ * * Changes made: * - Made sure ngccProcessor is optional. + * - Use custom cacheCompilerHost instead of the one provided by ng-packagr. */ import type { @@ -17,15 +18,13 @@ import { PackageNode, } from 'ng-packagr/lib/ng-package/nodes'; import { NgccProcessor } from 'ng-packagr/lib/ngc/ngcc-processor'; -import { StylesheetProcessor } from 'ng-packagr/lib/styles/stylesheet-processor'; -import { - augmentProgramWithVersioning, - cacheCompilerHost, -} from 'ng-packagr/lib/ts/cache-compiler-host'; +import { augmentProgramWithVersioning } from 'ng-packagr/lib/ts/cache-compiler-host'; import { ngccTransformCompilerHost } from 'ng-packagr/lib/ts/ngcc-transform-compiler-host'; import * as log from 'ng-packagr/lib/utils/log'; import { ngCompilerCli } from 'ng-packagr/lib/utils/ng-compiler-cli'; import * as ts from 'typescript'; +import { StylesheetProcessor } from '../styles/stylesheet-processor'; +import { cacheCompilerHost } from '../ts/cache-compiler-host'; export async function compileSourceFiles( graph: BuildGraph, diff --git a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ts/cache-compiler-host.ts b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ts/cache-compiler-host.ts new file mode 100644 index 0000000000..8a2b6e6eaf --- /dev/null +++ b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ts/cache-compiler-host.ts @@ -0,0 +1,219 @@ +/** + * Adapted from the original ng-packagr source. + * + * Changes made: + * - Changed filePath passed to the StylesheetProcessor.parse when is a .ts file and inlineStyleLanguage is set. + */ + +import type { CompilerHost, CompilerOptions } from '@angular/compiler-cli'; +import { createHash } from 'crypto'; +import { FileCache } from 'ng-packagr/lib/file-system/file-cache'; +import { BuildGraph } from 'ng-packagr/lib/graph/build-graph'; +import { Node } from 'ng-packagr/lib/graph/node'; +import { EntryPointNode, fileUrl } from 'ng-packagr/lib/ng-package/nodes'; +import { ensureUnixPath } from 'ng-packagr/lib/utils/path'; +import * as path from 'path'; +import * as ts from 'typescript'; +import { + InlineStyleLanguage, + StylesheetProcessor, +} from '../styles/stylesheet-processor'; + +export function cacheCompilerHost( + graph: BuildGraph, + entryPoint: EntryPointNode, + compilerOptions: CompilerOptions, + moduleResolutionCache: ts.ModuleResolutionCache, + stylesheetProcessor?: StylesheetProcessor, + inlineStyleLanguage?: InlineStyleLanguage, + sourcesFileCache: FileCache = entryPoint.cache.sourcesFileCache +): CompilerHost { + const compilerHost = ts.createIncrementalCompilerHost(compilerOptions); + + const getNode = (fileName: string) => { + const nodeUri = fileUrl(ensureUnixPath(fileName)); + let node = graph.get(nodeUri); + + if (!node) { + node = new Node(nodeUri); + graph.put(node); + } + + return node; + }; + + const addDependee = (fileName: string) => { + const node = getNode(fileName); + entryPoint.dependsOn(node); + }; + + return { + ...compilerHost, + + // ts specific + fileExists: (fileName: string) => { + const cache = sourcesFileCache.getOrCreate(fileName); + if (cache.exists === undefined) { + cache.exists = compilerHost.fileExists.call(this, fileName); + } + + return cache.exists; + }, + + getSourceFile: (fileName: string, languageVersion: ts.ScriptTarget) => { + addDependee(fileName); + const cache = sourcesFileCache.getOrCreate(fileName); + if (!cache.sourceFile) { + cache.sourceFile = compilerHost.getSourceFile.call( + this, + fileName, + languageVersion + ); + } + + return cache.sourceFile; + }, + + writeFile: ( + fileName: string, + data: string, + writeByteOrderMark: boolean, + onError?: (message: string) => void, + sourceFiles?: ReadonlyArray + ) => { + if (fileName.endsWith('.d.ts')) { + sourceFiles.forEach((source) => { + const cache = sourcesFileCache.getOrCreate(source.fileName); + if (!cache.declarationFileName) { + cache.declarationFileName = ensureUnixPath(fileName); + } + }); + } else { + fileName = fileName.replace(/\.js(\.map)?$/, '.mjs$1'); + const outputCache = entryPoint.cache.outputCache; + + outputCache.set(fileName, { + content: data, + version: createHash('sha256').update(data).digest('hex'), + }); + } + + compilerHost.writeFile.call( + this, + fileName, + data, + writeByteOrderMark, + onError, + sourceFiles + ); + }, + + readFile: (fileName: string) => { + addDependee(fileName); + const cache = sourcesFileCache.getOrCreate(fileName); + if (cache.content === undefined) { + cache.content = compilerHost.readFile.call(this, fileName); + } + + return cache.content; + }, + + resolveModuleNames: (moduleNames: string[], containingFile: string) => { + return moduleNames.map((moduleName) => { + const { resolvedModule } = ts.resolveModuleName( + moduleName, + ensureUnixPath(containingFile), + compilerOptions, + compilerHost, + moduleResolutionCache + ); + + return resolvedModule; + }); + }, + + resourceNameToFileName: ( + resourceName: string, + containingFilePath: string + ) => { + const resourcePath = path.resolve( + path.dirname(containingFilePath), + resourceName + ); + const containingNode = getNode(containingFilePath); + const resourceNode = getNode(resourcePath); + containingNode.dependsOn(resourceNode); + + return resourcePath; + }, + + readResource: async (fileName: string) => { + addDependee(fileName); + + const cache = sourcesFileCache.getOrCreate(fileName); + if (cache.content === undefined) { + if (/(?:html?|svg)$/.test(path.extname(fileName))) { + // template + cache.content = compilerHost.readFile.call(this, fileName); + } else { + // stylesheet + cache.content = await stylesheetProcessor.process({ + filePath: fileName, + content: compilerHost.readFile.call(this, fileName), + }); + } + + if (cache.content === undefined) { + throw new Error(`Cannot read file ${fileName}.`); + } + + cache.exists = true; + } + + return cache.content; + }, + transformResource: async (data, context) => { + if (context.resourceFile || context.type !== 'style') { + return null; + } + + if (inlineStyleLanguage) { + const key = createHash('sha1').update(data).digest('hex'); + const fileName = `${context.containingFile}-${key}.${inlineStyleLanguage}`; + const cache = sourcesFileCache.getOrCreate(fileName); + if (cache.content === undefined) { + cache.content = await stylesheetProcessor.process({ + filePath: context.containingFile, // @leosvelperez: changed from fileName + content: data, + }); + + const virtualFileNode = getNode(fileName); + const containingFileNode = getNode(context.containingFile); + virtualFileNode.dependsOn(containingFileNode); + } + + cache.exists = true; + + return { content: cache.content }; + } + + return null; + }, + }; +} + +export function augmentProgramWithVersioning(program: ts.Program): void { + const baseGetSourceFiles = program.getSourceFiles; + program.getSourceFiles = function (...parameters) { + const files: readonly (ts.SourceFile & { version?: string })[] = + baseGetSourceFiles(...parameters); + + for (const file of files) { + if (file.version === undefined) { + file.version = createHash('sha256').update(file.text).digest('hex'); + } + } + + return files; + }; +} 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 7787be3fb7..9d3c47228b 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,13 +14,13 @@ import { isEntryPoint, isEntryPointInProgress, } from 'ng-packagr/lib/ng-package/nodes'; -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 { ngccCompilerCli } from 'ng-packagr/lib/utils/ng-compiler-cli'; import * as ora from 'ora'; 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'; @@ -85,7 +85,7 @@ export const compileNgcTransformFactory = ( declaration: true, target: ts.ScriptTarget.ES2020, }, - entryPoint.cache.stylesheetProcessor, + entryPoint.cache.stylesheetProcessor as any, ngccProcessor, options.watch ); diff --git a/packages/angular/src/executors/package/ng-packagr-adjustments/ngc/compile-source-files.ts b/packages/angular/src/executors/package/ng-packagr-adjustments/ngc/compile-source-files.ts new file mode 100644 index 0000000000..01be8118e8 --- /dev/null +++ b/packages/angular/src/executors/package/ng-packagr-adjustments/ngc/compile-source-files.ts @@ -0,0 +1,221 @@ +/** + * Adapted from the original ng-packagr source. + * + * Changes made: + * - Use custom cacheCompilerHost instead of the one provided by ng-packagr. + */ + +import type { + CompilerOptions, + ParsedConfiguration, +} from '@angular/compiler-cli'; +import { BuildGraph } from 'ng-packagr/lib/graph/build-graph'; +import { + EntryPointNode, + isEntryPointInProgress, + isPackage, + PackageNode, +} from 'ng-packagr/lib/ng-package/nodes'; +import { NgccProcessor } from 'ng-packagr/lib/ngc/ngcc-processor'; +import { ngccTransformCompilerHost } from 'ng-packagr/lib/ts/ngcc-transform-compiler-host'; +import * as log from 'ng-packagr/lib/utils/log'; +import { ngCompilerCli } from 'ng-packagr/lib/utils/ng-compiler-cli'; +import * as ts from 'typescript'; +import { StylesheetProcessor } from '../styles/stylesheet-processor'; +import { + augmentProgramWithVersioning, + cacheCompilerHost, +} from '../ts/cache-compiler-host'; + +export async function compileSourceFiles( + graph: BuildGraph, + tsConfig: ParsedConfiguration, + moduleResolutionCache: ts.ModuleResolutionCache, + extraOptions?: Partial, + stylesheetProcessor?: StylesheetProcessor, + ngccProcessor?: NgccProcessor, + watch?: boolean +) { + const { NgtscProgram, formatDiagnostics } = await ngCompilerCli(); + + const tsConfigOptions: CompilerOptions = { + ...tsConfig.options, + ...extraOptions, + }; + const entryPoint: EntryPointNode = graph.find(isEntryPointInProgress()); + const ngPackageNode: PackageNode = graph.find(isPackage); + const inlineStyleLanguage = ngPackageNode.data.inlineStyleLanguage; + + const tsCompilerHost = ngccTransformCompilerHost( + cacheCompilerHost( + graph, + entryPoint, + tsConfigOptions, + moduleResolutionCache, + stylesheetProcessor, + inlineStyleLanguage + ), + tsConfigOptions, + ngccProcessor, + moduleResolutionCache + ); + + const cache = entryPoint.cache; + const sourceFileCache = cache.sourcesFileCache; + + // Create the Angular specific program that contains the Angular compiler + const angularProgram = new NgtscProgram( + tsConfig.rootNames, + tsConfigOptions, + tsCompilerHost, + cache.oldNgtscProgram + ); + + const angularCompiler = angularProgram.compiler; + const { ignoreForDiagnostics, ignoreForEmit } = angularCompiler; + + // SourceFile versions are required for builder programs. + // The wrapped host inside NgtscProgram adds additional files that will not have versions. + const typeScriptProgram = angularProgram.getTsProgram(); + augmentProgramWithVersioning(typeScriptProgram); + + let builder: ts.BuilderProgram | ts.EmitAndSemanticDiagnosticsBuilderProgram; + if (watch) { + builder = cache.oldBuilder = + ts.createEmitAndSemanticDiagnosticsBuilderProgram( + typeScriptProgram, + tsCompilerHost, + cache.oldBuilder + ); + cache.oldNgtscProgram = angularProgram; + } else { + // When not in watch mode, the startup cost of the incremental analysis can be avoided by + // using an abstract builder that only wraps a TypeScript program. + builder = ts.createAbstractBuilder(typeScriptProgram, tsCompilerHost); + } + + // Update semantic diagnostics cache + const affectedFiles = new Set(); + + // Analyze affected files when in watch mode for incremental type checking + if ('getSemanticDiagnosticsOfNextAffectedFile' in builder) { + // eslint-disable-next-line no-constant-condition + while (true) { + const result = builder.getSemanticDiagnosticsOfNextAffectedFile( + undefined, + (sourceFile) => { + // If the affected file is a TTC shim, add the shim's original source file. + // This ensures that changes that affect TTC are typechecked even when the changes + // are otherwise unrelated from a TS perspective and do not result in Ivy codegen changes. + // For example, changing @Input property types of a directive used in another component's + // template. + if ( + ignoreForDiagnostics.has(sourceFile) && + sourceFile.fileName.endsWith('.ngtypecheck.ts') + ) { + // This file name conversion relies on internal compiler logic and should be converted + // to an official method when available. 15 is length of `.ngtypecheck.ts` + const originalFilename = sourceFile.fileName.slice(0, -15) + '.ts'; + const originalSourceFile = builder.getSourceFile(originalFilename); + if (originalSourceFile) { + affectedFiles.add(originalSourceFile); + } + + return true; + } + + return false; + } + ); + + if (!result) { + break; + } + + affectedFiles.add(result.affected as ts.SourceFile); + } + } + + // Collect program level diagnostics + const allDiagnostics: ts.Diagnostic[] = [ + ...angularCompiler.getOptionDiagnostics(), + ...builder.getOptionsDiagnostics(), + ...builder.getGlobalDiagnostics(), + ]; + + // Required to support asynchronous resource loading + // Must be done before creating transformers or getting template diagnostics + await angularCompiler.analyzeAsync(); + + // Collect source file specific diagnostics + for (const sourceFile of builder.getSourceFiles()) { + if (!ignoreForDiagnostics.has(sourceFile)) { + allDiagnostics.push( + ...builder.getSyntacticDiagnostics(sourceFile), + ...builder.getSemanticDiagnostics(sourceFile) + ); + } + + if (sourceFile.isDeclarationFile) { + continue; + } + + // Collect sources that are required to be emitted + if ( + !ignoreForEmit.has(sourceFile) && + !angularCompiler.incrementalDriver.safeToSkipEmit(sourceFile) + ) { + // If required to emit, diagnostics may have also changed + if (!ignoreForDiagnostics.has(sourceFile)) { + affectedFiles.add(sourceFile); + } + } else if ( + sourceFileCache && + !affectedFiles.has(sourceFile) && + !ignoreForDiagnostics.has(sourceFile) + ) { + // Use cached Angular diagnostics for unchanged and unaffected files + const angularDiagnostics = + sourceFileCache.getAngularDiagnostics(sourceFile); + if (angularDiagnostics?.length) { + allDiagnostics.push(...angularDiagnostics); + } + } + } + + // Collect new Angular diagnostics for files affected by changes + for (const affectedFile of affectedFiles) { + const angularDiagnostics = angularCompiler.getDiagnosticsForFile( + affectedFile, + /** OptimizeFor.WholeProgram */ 1 + ); + + allDiagnostics.push(...angularDiagnostics); + sourceFileCache.updateAngularDiagnostics(affectedFile, angularDiagnostics); + } + + const otherDiagnostics = []; + const errorDiagnostics = []; + for (const diagnostic of allDiagnostics) { + if (diagnostic.category === ts.DiagnosticCategory.Error) { + errorDiagnostics.push(diagnostic); + } else { + otherDiagnostics.push(diagnostic); + } + } + + if (otherDiagnostics.length) { + log.msg(formatDiagnostics(errorDiagnostics)); + } + + if (errorDiagnostics.length) { + throw new Error(formatDiagnostics(errorDiagnostics)); + } + + const transformers = angularCompiler.prepareEmit().transformers; + for (const sourceFile of builder.getSourceFiles()) { + if (!ignoreForEmit.has(sourceFile)) { + builder.emit(sourceFile, undefined, undefined, undefined, transformers); + } + } +} diff --git a/packages/angular/src/executors/package/ng-packagr-adjustments/ts/cache-compiler-host.ts b/packages/angular/src/executors/package/ng-packagr-adjustments/ts/cache-compiler-host.ts new file mode 100644 index 0000000000..8a2b6e6eaf --- /dev/null +++ b/packages/angular/src/executors/package/ng-packagr-adjustments/ts/cache-compiler-host.ts @@ -0,0 +1,219 @@ +/** + * Adapted from the original ng-packagr source. + * + * Changes made: + * - Changed filePath passed to the StylesheetProcessor.parse when is a .ts file and inlineStyleLanguage is set. + */ + +import type { CompilerHost, CompilerOptions } from '@angular/compiler-cli'; +import { createHash } from 'crypto'; +import { FileCache } from 'ng-packagr/lib/file-system/file-cache'; +import { BuildGraph } from 'ng-packagr/lib/graph/build-graph'; +import { Node } from 'ng-packagr/lib/graph/node'; +import { EntryPointNode, fileUrl } from 'ng-packagr/lib/ng-package/nodes'; +import { ensureUnixPath } from 'ng-packagr/lib/utils/path'; +import * as path from 'path'; +import * as ts from 'typescript'; +import { + InlineStyleLanguage, + StylesheetProcessor, +} from '../styles/stylesheet-processor'; + +export function cacheCompilerHost( + graph: BuildGraph, + entryPoint: EntryPointNode, + compilerOptions: CompilerOptions, + moduleResolutionCache: ts.ModuleResolutionCache, + stylesheetProcessor?: StylesheetProcessor, + inlineStyleLanguage?: InlineStyleLanguage, + sourcesFileCache: FileCache = entryPoint.cache.sourcesFileCache +): CompilerHost { + const compilerHost = ts.createIncrementalCompilerHost(compilerOptions); + + const getNode = (fileName: string) => { + const nodeUri = fileUrl(ensureUnixPath(fileName)); + let node = graph.get(nodeUri); + + if (!node) { + node = new Node(nodeUri); + graph.put(node); + } + + return node; + }; + + const addDependee = (fileName: string) => { + const node = getNode(fileName); + entryPoint.dependsOn(node); + }; + + return { + ...compilerHost, + + // ts specific + fileExists: (fileName: string) => { + const cache = sourcesFileCache.getOrCreate(fileName); + if (cache.exists === undefined) { + cache.exists = compilerHost.fileExists.call(this, fileName); + } + + return cache.exists; + }, + + getSourceFile: (fileName: string, languageVersion: ts.ScriptTarget) => { + addDependee(fileName); + const cache = sourcesFileCache.getOrCreate(fileName); + if (!cache.sourceFile) { + cache.sourceFile = compilerHost.getSourceFile.call( + this, + fileName, + languageVersion + ); + } + + return cache.sourceFile; + }, + + writeFile: ( + fileName: string, + data: string, + writeByteOrderMark: boolean, + onError?: (message: string) => void, + sourceFiles?: ReadonlyArray + ) => { + if (fileName.endsWith('.d.ts')) { + sourceFiles.forEach((source) => { + const cache = sourcesFileCache.getOrCreate(source.fileName); + if (!cache.declarationFileName) { + cache.declarationFileName = ensureUnixPath(fileName); + } + }); + } else { + fileName = fileName.replace(/\.js(\.map)?$/, '.mjs$1'); + const outputCache = entryPoint.cache.outputCache; + + outputCache.set(fileName, { + content: data, + version: createHash('sha256').update(data).digest('hex'), + }); + } + + compilerHost.writeFile.call( + this, + fileName, + data, + writeByteOrderMark, + onError, + sourceFiles + ); + }, + + readFile: (fileName: string) => { + addDependee(fileName); + const cache = sourcesFileCache.getOrCreate(fileName); + if (cache.content === undefined) { + cache.content = compilerHost.readFile.call(this, fileName); + } + + return cache.content; + }, + + resolveModuleNames: (moduleNames: string[], containingFile: string) => { + return moduleNames.map((moduleName) => { + const { resolvedModule } = ts.resolveModuleName( + moduleName, + ensureUnixPath(containingFile), + compilerOptions, + compilerHost, + moduleResolutionCache + ); + + return resolvedModule; + }); + }, + + resourceNameToFileName: ( + resourceName: string, + containingFilePath: string + ) => { + const resourcePath = path.resolve( + path.dirname(containingFilePath), + resourceName + ); + const containingNode = getNode(containingFilePath); + const resourceNode = getNode(resourcePath); + containingNode.dependsOn(resourceNode); + + return resourcePath; + }, + + readResource: async (fileName: string) => { + addDependee(fileName); + + const cache = sourcesFileCache.getOrCreate(fileName); + if (cache.content === undefined) { + if (/(?:html?|svg)$/.test(path.extname(fileName))) { + // template + cache.content = compilerHost.readFile.call(this, fileName); + } else { + // stylesheet + cache.content = await stylesheetProcessor.process({ + filePath: fileName, + content: compilerHost.readFile.call(this, fileName), + }); + } + + if (cache.content === undefined) { + throw new Error(`Cannot read file ${fileName}.`); + } + + cache.exists = true; + } + + return cache.content; + }, + transformResource: async (data, context) => { + if (context.resourceFile || context.type !== 'style') { + return null; + } + + if (inlineStyleLanguage) { + const key = createHash('sha1').update(data).digest('hex'); + const fileName = `${context.containingFile}-${key}.${inlineStyleLanguage}`; + const cache = sourcesFileCache.getOrCreate(fileName); + if (cache.content === undefined) { + cache.content = await stylesheetProcessor.process({ + filePath: context.containingFile, // @leosvelperez: changed from fileName + content: data, + }); + + const virtualFileNode = getNode(fileName); + const containingFileNode = getNode(context.containingFile); + virtualFileNode.dependsOn(containingFileNode); + } + + cache.exists = true; + + return { content: cache.content }; + } + + return null; + }, + }; +} + +export function augmentProgramWithVersioning(program: ts.Program): void { + const baseGetSourceFiles = program.getSourceFiles; + program.getSourceFiles = function (...parameters) { + const files: readonly (ts.SourceFile & { version?: string })[] = + baseGetSourceFiles(...parameters); + + for (const file of files) { + if (file.version === undefined) { + file.version = createHash('sha256').update(file.text).digest('hex'); + } + } + + return files; + }; +}