fix(angular): fix tailwind css support in libraries using components with inline styles (#8393)

This commit is contained in:
Leosvel Pérez Espinosa 2022-01-06 10:17:15 +00:00 committed by GitHub
parent 77529a1770
commit 80f20db51a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 1048 additions and 393 deletions

View File

@ -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: '<button class="custom-btn text-white ${buttonBgColor}">Click me!</button>',
styles: [\`
.custom-btn {
@apply m-md p-sm;
}
\`]
})
export class FooComponent {}
`
);
updateFile(
`libs/${lib}/src/lib/${lib}.module.ts`,
`import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FooComponent } from './foo.component';
@NgModule({
imports: [CommonModule],
declarations: [FooComponent],
exports: [FooComponent],
})
export class LibModule {}
`
);
updateFile(
`libs/${lib}/src/index.ts`,
`export * from './lib/foo.component';
export * from './lib/${lib}.module';
`
);
};
const assertLibComponentStyles = (
lib: string,
libSpacing: typeof spacing['root']
) => {
const builtComponentContent = readFile(
`dist/libs/${lib}/esm2020/lib/foo.component.mjs`
);
let expectedStylesRegex = new RegExp(
`styles: \\[\\"\\.custom\\-btn(\\[_ngcontent\\-%COMP%\\])?{margin:${libSpacing.md};padding:${libSpacing.sm}}(\\\\n)?\\"\\]`
);
expect(builtComponentContent).toMatch(expectedStylesRegex);
};
it('should generate a buildable library with tailwind and build correctly', () => {
runCLI(
`generate @nrwl/angular:lib ${buildLibWithTailwind.name} --buildable --add-tailwind --no-interactive`
);
updateTailwindConfig(
`libs/${buildLibWithTailwind.name}/tailwind.config.js`,
spacing.projectVariant1
);
createLibComponent(
buildLibWithTailwind.name,
buildLibWithTailwind.buttonBgColor
);
runCLI(`build ${buildLibWithTailwind.name}`);
assertLibComponentStyles(
buildLibWithTailwind.name,
spacing.projectVariant1
);
});
it('should set up tailwind in a previously generated buildable library and build correctly', () => {
const buildLibSetupTailwind = uniq('build-lib-setup-tailwind');
runCLI(
`generate @nrwl/angular:lib ${buildLibSetupTailwind} --buildable --no-interactive`
);
runCLI(
`generate @nrwl/angular:setup-tailwind ${buildLibSetupTailwind} --no-interactive`
);
updateTailwindConfig(
`libs/${buildLibSetupTailwind}/tailwind.config.js`,
spacing.projectVariant2
);
createLibComponent(buildLibSetupTailwind);
runCLI(`build ${buildLibSetupTailwind}`);
assertLibComponentStyles(buildLibSetupTailwind, spacing.projectVariant2);
});
it('should correctly build a buildable library with a tailwind.config.js file in the project root or workspace root', () => {
const buildLibNoProjectConfig = uniq('build-lib-no-project-config');
runCLI(
`generate @nrwl/angular:lib ${buildLibNoProjectConfig} --buildable --no-interactive`
);
createTailwindConfigFile(
`libs/${buildLibNoProjectConfig}/tailwind.config.js`,
spacing.projectVariant3
);
createLibComponent(buildLibNoProjectConfig);
runCLI(`build ${buildLibNoProjectConfig}`);
assertLibComponentStyles(
buildLibNoProjectConfig,
spacing.projectVariant3
);
// remove tailwind.config.js file from the project root to test the one in the workspace root
removeFile(`libs/${buildLibNoProjectConfig}/tailwind.config.js`);
runCLI(`build ${buildLibNoProjectConfig}`);
assertLibComponentStyles(buildLibNoProjectConfig, spacing.root);
});
it('should generate a publishable library with tailwind and build correctly', () => {
runCLI(
`generate @nrwl/angular:lib ${pubLibWithTailwind.name} --publishable --add-tailwind --importPath=@${project}/${pubLibWithTailwind.name} --no-interactive`
);
updateTailwindConfig(
`libs/${pubLibWithTailwind.name}/tailwind.config.js`,
spacing.projectVariant1
);
createLibComponent(
pubLibWithTailwind.name,
pubLibWithTailwind.buttonBgColor
);
runCLI(`build ${pubLibWithTailwind.name}`);
assertLibComponentStyles(
pubLibWithTailwind.name,
spacing.projectVariant1
);
});
it('should set up tailwind in a previously generated publishable library and build correctly', () => {
const pubLibSetupTailwind = uniq('pub-lib-setup-tailwind');
runCLI(
`generate @nrwl/angular:lib ${pubLibSetupTailwind} --publishable --importPath=@${project}/${pubLibSetupTailwind} --no-interactive`
);
runCLI(
`generate @nrwl/angular:setup-tailwind ${pubLibSetupTailwind} --no-interactive`
);
updateTailwindConfig(
`libs/${pubLibSetupTailwind}/tailwind.config.js`,
spacing.projectVariant2
);
createLibComponent(pubLibSetupTailwind);
runCLI(`build ${pubLibSetupTailwind}`);
assertLibComponentStyles(pubLibSetupTailwind, spacing.projectVariant2);
});
it('should correctly build a publishable library with a tailwind.config.js file in the project root or workspace root', () => {
const pubLibNoProjectConfig = uniq('pub-lib-no-project-config');
runCLI(
`generate @nrwl/angular:lib ${pubLibNoProjectConfig} --publishable --importPath=@${project}/${pubLibNoProjectConfig} --no-interactive`
);
createTailwindConfigFile(
`libs/${pubLibNoProjectConfig}/tailwind.config.js`,
spacing.projectVariant3
);
createLibComponent(pubLibNoProjectConfig);
runCLI(`build ${pubLibNoProjectConfig}`);
assertLibComponentStyles(pubLibNoProjectConfig, spacing.projectVariant3);
// remove tailwind.config.js file from the project root to test the one in the workspace root
removeFile(`libs/${pubLibNoProjectConfig}/tailwind.config.js`);
runCLI(`build ${pubLibNoProjectConfig}`);
assertLibComponentStyles(pubLibNoProjectConfig, spacing.root);
});
});
describe('Applications', () => {
const updateAppComponent = (app: string) => {
updateFile(
`apps/${app}/src/app/app.component.html`,
`<button class="custom-btn text-white">Click me!</button>`
);
updateFile(
`apps/${app}/src/app/app.component.css`,
`.custom-btn {
@apply m-md p-sm;
}`
);
};
const readAppStylesBundle = (app: string) => {
const stylesBundlePath = listFiles(`dist/apps/${app}`).find((file) =>
file.startsWith('styles.')
);
const stylesBundle = readFile(`dist/apps/${app}/${stylesBundlePath}`);
return stylesBundle;
};
const assertAppComponentStyles = (
app: string,
appSpacing: typeof spacing['root']
) => {
const mainBundlePath = listFiles(`dist/apps/${app}`).find((file) =>
file.startsWith('main.')
);
const mainBundle = readFile(`dist/apps/${app}/${mainBundlePath}`);
let expectedStylesRegex = new RegExp(
`styles:\\[\\"\\.custom\\-btn\\[_ngcontent\\-%COMP%\\]{margin:${appSpacing.md};padding:${appSpacing.sm}}\\"\\]`
);
expect(mainBundle).toMatch(expectedStylesRegex);
};
it('should build correctly and only output the tailwind utilities used', () => {
const appWithTailwind = uniq('app-with-tailwind');
runCLI(
`generate @nrwl/angular:app ${appWithTailwind} --add-tailwind --no-interactive`
);
updateTailwindConfig(
`apps/${appWithTailwind}/tailwind.config.js`,
spacing.projectVariant1
);
updateFile(
`apps/${appWithTailwind}/src/app/app.module.ts`,
`import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { LibModule as LibModule1 } from '@${project}/${buildLibWithTailwind.name}';
import { LibModule as LibModule2 } from '@${project}/${pubLibWithTailwind.name}';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, LibModule1, LibModule2],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
`
);
updateAppComponent(appWithTailwind);
runCLI(`build ${appWithTailwind}`);
assertAppComponentStyles(appWithTailwind, spacing.projectVariant1);
let stylesBundle = readAppStylesBundle(appWithTailwind);
expect(stylesBundle).toContain('.text-white');
expect(stylesBundle).not.toContain('.text-black');
expect(stylesBundle).toContain(`.${buildLibWithTailwind.buttonBgColor}`);
expect(stylesBundle).toContain(`.${pubLibWithTailwind.buttonBgColor}`);
expect(stylesBundle).not.toContain(`.${defaultButtonBgColor}`);
});
});
// let project: string;
//
// const defaultButtonBgColor = 'bg-blue-700';
//
// const buildLibWithTailwind = {
// name: uniq('build-lib-with-tailwind'),
// buttonBgColor: 'bg-green-800',
// };
// const pubLibWithTailwind = {
// name: uniq('pub-lib-with-tailwind'),
// buttonBgColor: 'bg-red-900',
// };
//
// const spacing = {
// root: {
// sm: '2px',
// md: '4px',
// lg: '8px',
// },
// projectVariant1: {
// sm: '1px',
// md: '2px',
// lg: '4px',
// },
// projectVariant2: {
// sm: '4px',
// md: '8px',
// lg: '16px',
// },
// projectVariant3: {
// sm: '8px',
// md: '16px',
// lg: '32px',
// },
// };
//
// const createWorkspaceTailwindConfigFile = () => {
// const tailwindConfigFile = 'tailwind.config.js';
//
// const tailwindConfig = `module.exports = {
// mode: 'jit',
// purge: ['./apps/**/*.{html,ts}', './libs/**/*.{html,ts}'],
// darkMode: false,
// theme: {
// spacing: {
// sm: '${spacing.root.sm}',
// md: '${spacing.root.md}',
// lg: '${spacing.root.lg}',
// },
// },
// variants: { extend: {} },
// plugins: [],
// };
// `;
//
// updateFile(tailwindConfigFile, tailwindConfig);
// };
//
// const createTailwindConfigFile = (
// tailwindConfigFile = 'tailwind.config.js',
// libSpacing: typeof spacing['projectVariant1']
// ) => {
// const tailwindConfig = `const { createGlobPatternsForDependencies } = require('@nrwl/angular/tailwind');
// const { join } = require('path');
//
// module.exports = {
// mode: 'jit',
// purge: [
// join(__dirname, 'src/**/*.{html,ts}'),
// ...createGlobPatternsForDependencies(__dirname),
// ],
// darkMode: false,
// theme: {
// spacing: {
// sm: '${libSpacing.sm}',
// md: '${libSpacing.md}',
// lg: '${libSpacing.lg}',
// },
// },
// variants: { extend: {} },
// plugins: [],
// };
// `;
//
// updateFile(tailwindConfigFile, tailwindConfig);
// };
//
// const updateTailwindConfig = (
// tailwindConfigPath: string,
// projectSpacing: typeof spacing['root']
// ) => {
// const tailwindConfig = readFile(tailwindConfigPath);
//
// const tailwindConfigUpdated = tailwindConfig.replace(
// 'theme: {',
// `theme: {
// spacing: {
// sm: '${projectSpacing.sm}',
// md: '${projectSpacing.md}',
// lg: '${projectSpacing.lg}',
// },`
// );
//
// updateFile(tailwindConfigPath, tailwindConfigUpdated);
// };
//
// beforeAll(() => {
// project = newProject();
//
// // Create tailwind config in the workspace root
// createWorkspaceTailwindConfigFile();
// });
//
// afterAll(() => cleanupProject());
//
// describe('Libraries', () => {
// const createLibComponent = (
// lib: string,
// buttonBgColor: string = defaultButtonBgColor
// ) => {
// updateFile(
// `libs/${lib}/src/lib/foo.component.ts`,
// `import { Component } from '@angular/core';
//
// @Component({
// selector: '${project}-foo',
// template: '<button class="custom-btn text-white ${buttonBgColor}">Click me!</button>',
// styles: [\`
// .custom-btn {
// @apply m-md p-sm;
// }
// \`]
// })
// export class FooComponent {}
// `
// );
//
// updateFile(
// `libs/${lib}/src/lib/${lib}.module.ts`,
// `import { NgModule } from '@angular/core';
// import { CommonModule } from '@angular/common';
// import { FooComponent } from './foo.component';
//
// @NgModule({
// imports: [CommonModule],
// declarations: [FooComponent],
// exports: [FooComponent],
// })
// export class LibModule {}
// `
// );
//
// updateFile(
// `libs/${lib}/src/index.ts`,
// `export * from './lib/foo.component';
// export * from './lib/${lib}.module';
// `
// );
// };
//
// const assertLibComponentStyles = (
// lib: string,
// libSpacing: typeof spacing['root']
// ) => {
// const builtComponentContent = readFile(
// `dist/libs/${lib}/esm2020/lib/foo.component.mjs`
// );
// let expectedStylesRegex = new RegExp(
// `styles: \\[\\"\\.custom\\-btn(\\[_ngcontent\\-%COMP%\\])?{margin:${libSpacing.md};padding:${libSpacing.sm}}(\\\\n)?\\"\\]`
// );
//
// expect(builtComponentContent).toMatch(expectedStylesRegex);
// };
//
// it('should generate a buildable library with tailwind and build correctly', () => {
// runCLI(
// `generate @nrwl/angular:lib ${buildLibWithTailwind.name} --buildable --add-tailwind --no-interactive`
// );
// updateTailwindConfig(
// `libs/${buildLibWithTailwind.name}/tailwind.config.js`,
// spacing.projectVariant1
// );
// createLibComponent(
// buildLibWithTailwind.name,
// buildLibWithTailwind.buttonBgColor
// );
//
// runCLI(`build ${buildLibWithTailwind.name}`);
//
// assertLibComponentStyles(
// buildLibWithTailwind.name,
// spacing.projectVariant1
// );
// });
//
// it('should set up tailwind in a previously generated buildable library and build correctly', () => {
// const buildLibSetupTailwind = uniq('build-lib-setup-tailwind');
// runCLI(
// `generate @nrwl/angular:lib ${buildLibSetupTailwind} --buildable --no-interactive`
// );
// runCLI(
// `generate @nrwl/angular:setup-tailwind ${buildLibSetupTailwind} --no-interactive`
// );
// updateTailwindConfig(
// `libs/${buildLibSetupTailwind}/tailwind.config.js`,
// spacing.projectVariant2
// );
// createLibComponent(buildLibSetupTailwind);
//
// runCLI(`build ${buildLibSetupTailwind}`);
//
// assertLibComponentStyles(buildLibSetupTailwind, spacing.projectVariant2);
// });
//
// it('should correctly build a buildable library with a tailwind.config.js file in the project root or workspace root', () => {
// const buildLibNoProjectConfig = uniq('build-lib-no-project-config');
// runCLI(
// `generate @nrwl/angular:lib ${buildLibNoProjectConfig} --buildable --no-interactive`
// );
// createTailwindConfigFile(
// `libs/${buildLibNoProjectConfig}/tailwind.config.js`,
// spacing.projectVariant3
// );
// createLibComponent(buildLibNoProjectConfig);
//
// runCLI(`build ${buildLibNoProjectConfig}`);
//
// assertLibComponentStyles(
// buildLibNoProjectConfig,
// spacing.projectVariant3
// );
//
// // remove tailwind.config.js file from the project root to test the one in the workspace root
// removeFile(`libs/${buildLibNoProjectConfig}/tailwind.config.js`);
//
// runCLI(`build ${buildLibNoProjectConfig}`);
//
// assertLibComponentStyles(buildLibNoProjectConfig, spacing.root);
// });
//
// it('should generate a publishable library with tailwind and build correctly', () => {
// runCLI(
// `generate @nrwl/angular:lib ${pubLibWithTailwind.name} --publishable --add-tailwind --importPath=@${project}/${pubLibWithTailwind.name} --no-interactive`
// );
// updateTailwindConfig(
// `libs/${pubLibWithTailwind.name}/tailwind.config.js`,
// spacing.projectVariant1
// );
// createLibComponent(
// pubLibWithTailwind.name,
// pubLibWithTailwind.buttonBgColor
// );
//
// runCLI(`build ${pubLibWithTailwind.name}`);
//
// assertLibComponentStyles(
// pubLibWithTailwind.name,
// spacing.projectVariant1
// );
// });
//
// it('should set up tailwind in a previously generated publishable library and build correctly', () => {
// const pubLibSetupTailwind = uniq('pub-lib-setup-tailwind');
// runCLI(
// `generate @nrwl/angular:lib ${pubLibSetupTailwind} --publishable --importPath=@${project}/${pubLibSetupTailwind} --no-interactive`
// );
// runCLI(
// `generate @nrwl/angular:setup-tailwind ${pubLibSetupTailwind} --no-interactive`
// );
// updateTailwindConfig(
// `libs/${pubLibSetupTailwind}/tailwind.config.js`,
// spacing.projectVariant2
// );
// createLibComponent(pubLibSetupTailwind);
//
// runCLI(`build ${pubLibSetupTailwind}`);
//
// assertLibComponentStyles(pubLibSetupTailwind, spacing.projectVariant2);
// });
//
// it('should correctly build a publishable library with a tailwind.config.js file in the project root or workspace root', () => {
// const pubLibNoProjectConfig = uniq('pub-lib-no-project-config');
// runCLI(
// `generate @nrwl/angular:lib ${pubLibNoProjectConfig} --publishable --importPath=@${project}/${pubLibNoProjectConfig} --no-interactive`
// );
// createTailwindConfigFile(
// `libs/${pubLibNoProjectConfig}/tailwind.config.js`,
// spacing.projectVariant3
// );
// createLibComponent(pubLibNoProjectConfig);
//
// runCLI(`build ${pubLibNoProjectConfig}`);
//
// assertLibComponentStyles(pubLibNoProjectConfig, spacing.projectVariant3);
//
// // remove tailwind.config.js file from the project root to test the one in the workspace root
// removeFile(`libs/${pubLibNoProjectConfig}/tailwind.config.js`);
//
// runCLI(`build ${pubLibNoProjectConfig}`);
//
// assertLibComponentStyles(pubLibNoProjectConfig, spacing.root);
// });
// });
//
// describe('Applications', () => {
// const updateAppComponent = (app: string) => {
// updateFile(
// `apps/${app}/src/app/app.component.html`,
// `<button class="custom-btn text-white">Click me!</button>`
// );
//
// updateFile(
// `apps/${app}/src/app/app.component.css`,
// `.custom-btn {
// @apply m-md p-sm;
// }`
// );
// };
//
// const readAppStylesBundle = (app: string) => {
// const stylesBundlePath = listFiles(`dist/apps/${app}`).find((file) =>
// file.startsWith('styles.')
// );
// const stylesBundle = readFile(`dist/apps/${app}/${stylesBundlePath}`);
//
// return stylesBundle;
// };
//
// const assertAppComponentStyles = (
// app: string,
// appSpacing: typeof spacing['root']
// ) => {
// const mainBundlePath = listFiles(`dist/apps/${app}`).find((file) =>
// file.startsWith('main.')
// );
// const mainBundle = readFile(`dist/apps/${app}/${mainBundlePath}`);
// let expectedStylesRegex = new RegExp(
// `styles:\\[\\"\\.custom\\-btn\\[_ngcontent\\-%COMP%\\]{margin:${appSpacing.md};padding:${appSpacing.sm}}\\"\\]`
// );
//
// expect(mainBundle).toMatch(expectedStylesRegex);
// };
//
// it('should build correctly and only output the tailwind utilities used', () => {
// const appWithTailwind = uniq('app-with-tailwind');
// runCLI(
// `generate @nrwl/angular:app ${appWithTailwind} --add-tailwind --no-interactive`
// );
// updateTailwindConfig(
// `apps/${appWithTailwind}/tailwind.config.js`,
// spacing.projectVariant1
// );
// updateFile(
// `apps/${appWithTailwind}/src/app/app.module.ts`,
// `import { NgModule } from '@angular/core';
// import { BrowserModule } from '@angular/platform-browser';
// import { LibModule as LibModule1 } from '@${project}/${buildLibWithTailwind.name}';
// import { LibModule as LibModule2 } from '@${project}/${pubLibWithTailwind.name}';
//
// import { AppComponent } from './app.component';
//
// @NgModule({
// declarations: [AppComponent],
// imports: [BrowserModule, LibModule1, LibModule2],
// providers: [],
// bootstrap: [AppComponent],
// })
// export class AppModule {}
// `
// );
// updateAppComponent(appWithTailwind);
//
// runCLI(`build ${appWithTailwind}`);
//
// assertAppComponentStyles(appWithTailwind, spacing.projectVariant1);
// let stylesBundle = readAppStylesBundle(appWithTailwind);
// expect(stylesBundle).toContain('.text-white');
// expect(stylesBundle).not.toContain('.text-black');
// expect(stylesBundle).toContain(`.${buildLibWithTailwind.buttonBgColor}`);
// expect(stylesBundle).toContain(`.${pubLibWithTailwind.buttonBgColor}`);
// expect(stylesBundle).not.toContain(`.${defaultButtonBgColor}`);
// });
// });
});

View File

@ -58,7 +58,7 @@ export const nxCompileNgcTransformFactory = (
declaration: true,
target: ts.ScriptTarget.ES2020,
},
entryPoint.cache.stylesheetProcessor,
entryPoint.cache.stylesheetProcessor as any,
null,
options.watch
);

View File

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

View File

@ -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<ts.SourceFile>
) => {
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;
};
}

View File

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

View File

@ -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<CompilerOptions>,
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<ts.SourceFile>();
// 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);
}
}
}

View File

@ -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<ts.SourceFile>
) => {
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;
};
}