diff --git a/e2e/angular/src/tailwind.test.ts b/e2e/angular/src/tailwind.test.ts index 4d14252b41..3de3ca8bb3 100644 --- a/e2e/angular/src/tailwind.test.ts +++ b/e2e/angular/src/tailwind.test.ts @@ -174,10 +174,13 @@ describe('Tailwind support', () => { const assertLibComponentStyles = ( lib: string, - libSpacing: (typeof spacing)['root'] + libSpacing: (typeof spacing)['root'], + isPublishable: boolean = true ) => { const builtComponentContent = readFile( - `dist/${lib}/fesm2022/${project}-${lib}.mjs` + isPublishable + ? `dist/${lib}/fesm2022/${project}-${lib}.mjs` + : `dist/${lib}/esm2022/lib/foo.component.mjs` ); let expectedStylesRegex = new RegExp( `styles: \\[\\"\\.custom\\-btn(\\[_ngcontent\\-%COMP%\\])?{margin:${libSpacing.md};padding:${libSpacing.sm}}(\\\\n)?\\"\\]` @@ -203,7 +206,8 @@ describe('Tailwind support', () => { assertLibComponentStyles( buildLibWithTailwind.name, - spacing.projectVariant1 + spacing.projectVariant1, + false ); }); @@ -223,7 +227,11 @@ describe('Tailwind support', () => { runCLI(`build ${buildLibSetupTailwind}`); - assertLibComponentStyles(buildLibSetupTailwind, spacing.projectVariant2); + assertLibComponentStyles( + buildLibSetupTailwind, + spacing.projectVariant2, + false + ); }); it('should correctly build a buildable library with a tailwind.config.js file in the project root or workspace root', () => { @@ -241,7 +249,8 @@ describe('Tailwind support', () => { assertLibComponentStyles( buildLibNoProjectConfig, - spacing.projectVariant3 + spacing.projectVariant3, + false ); // remove tailwind.config.js file from the project root to test the one in the workspace root @@ -249,7 +258,7 @@ describe('Tailwind support', () => { runCLI(`build ${buildLibNoProjectConfig}`); - assertLibComponentStyles(buildLibNoProjectConfig, spacing.root); + assertLibComponentStyles(buildLibNoProjectConfig, spacing.root, false); }); it('should generate a publishable library with tailwind and build correctly', () => { diff --git a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/entry-point/entry-point.ts b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/entry-point/entry-point.ts new file mode 100644 index 0000000000..9e50928439 --- /dev/null +++ b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/entry-point/entry-point.ts @@ -0,0 +1,18 @@ +import { + type DestinationFiles, + NgEntryPoint as NgEntryPointBase, +} from 'ng-packagr/lib/ng-package/entry-point/entry-point'; +import { dirname } from 'node:path'; + +export class NgEntryPoint extends NgEntryPointBase { + /** + * Point the FESM2022 files to the ESM2022 files. + */ + public override get destinationFiles(): DestinationFiles { + const result = super.destinationFiles; + result.fesm2022 = result.esm2022; + result.fesm2022Dir = dirname(result.esm2022); + + return result; + } +} diff --git a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/pre-v19/ng-package/entry-point/write-bundles.di.ts b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/entry-point/write-bundles.di.ts similarity index 100% rename from packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/pre-v19/ng-package/entry-point/write-bundles.di.ts rename to packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/entry-point/write-bundles.di.ts diff --git a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/entry-point/write-bundles.transform.ts b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/entry-point/write-bundles.transform.ts new file mode 100644 index 0000000000..2db4b0614a --- /dev/null +++ b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-package/entry-point/write-bundles.transform.ts @@ -0,0 +1,58 @@ +/** + * Adapted from the original ng-packagr. + * + * Changes made: + * - Removed bundling altogether. + * - Write the ESM2022 outputs to the file system. + * - Fake the FESM2022 outputs pointing them to the ESM2022 outputs. + */ + +import { BuildGraph } from 'ng-packagr/lib/graph/build-graph'; +import { transformFromPromise } from 'ng-packagr/lib/graph/transform'; +import type { NgEntryPoint as NgEntryPointBase } from 'ng-packagr/lib/ng-package/entry-point/entry-point'; +import { isEntryPoint, isPackage } from 'ng-packagr/lib/ng-package/nodes'; +import type { NgPackagrOptions } from 'ng-packagr/lib/ng-package/options.di'; +import { NgPackage } from 'ng-packagr/lib/ng-package/package'; +import { mkdir, writeFile } from 'node:fs/promises'; +import { dirname } from 'node:path'; +import { NgEntryPoint } from './entry-point'; + +export const writeBundlesTransform = (_options: NgPackagrOptions) => + transformFromPromise(async (graph) => { + const updatedGraph = new BuildGraph(); + + for (const entry of graph.entries()) { + if (isEntryPoint(entry)) { + const entryPoint = toCustomNgEntryPoint(entry.data.entryPoint); + entry.data.entryPoint = entryPoint; + entry.data.destinationFiles = entryPoint.destinationFiles; + + for (const [path, outputCache] of entry.cache.outputCache.entries()) { + // write the outputs to the file system + await mkdir(dirname(path), { recursive: true }); + await writeFile(path, outputCache.content); + } + } else if (isPackage(entry)) { + entry.data = new NgPackage( + entry.data.src, + toCustomNgEntryPoint(entry.data.primary), + entry.data.secondaries.map((secondary) => + toCustomNgEntryPoint(secondary) + ) + ); + } + updatedGraph.put(entry); + } + + return updatedGraph; + }); + +function toCustomNgEntryPoint(entryPoint: NgEntryPointBase): NgEntryPoint { + return new NgEntryPoint( + entryPoint.packageJson, + entryPoint.ngPackageJson, + entryPoint.basePath, + // @ts-expect-error this is a TS private property, but it can be accessed at runtime + entryPoint.secondaryData + ); +} diff --git a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-packagr.ts b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-packagr.ts index 5b2e216f61..b56ef420fa 100644 --- a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-packagr.ts +++ b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/ng-packagr.ts @@ -1,35 +1,15 @@ import { NgPackagr, ngPackagr } from 'ng-packagr'; -import { getInstalledAngularVersionInfo } from '../../utilities/angular-version-utils'; export async function getNgPackagrInstance(): Promise { - const { major: angularMajorVersion } = getInstalledAngularVersionInfo(); - if (angularMajorVersion >= 19) { - const { STYLESHEET_PROCESSOR } = await import( - '../../utilities/ng-packagr/stylesheet-processor.di.js' - ); - - const packagr = ngPackagr(); - packagr.withProviders([STYLESHEET_PROCESSOR]); - - return packagr; - } - const { WRITE_BUNDLES_TRANSFORM } = await import( - './pre-v19/ng-package/entry-point/write-bundles.di.js' - ); - const { WRITE_PACKAGE_TRANSFORM } = await import( - './pre-v19/ng-package/entry-point/write-package.di.js' + './ng-package/entry-point/write-bundles.di.js' ); const { STYLESHEET_PROCESSOR } = await import( '../../utilities/ng-packagr/stylesheet-processor.di.js' ); const packagr = ngPackagr(); - packagr.withProviders([ - WRITE_BUNDLES_TRANSFORM, - WRITE_PACKAGE_TRANSFORM, - STYLESHEET_PROCESSOR, - ]); + packagr.withProviders([WRITE_BUNDLES_TRANSFORM, STYLESHEET_PROCESSOR]); return packagr; } diff --git a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/pre-v19/ng-package/entry-point/write-bundles.transform.ts b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/pre-v19/ng-package/entry-point/write-bundles.transform.ts deleted file mode 100644 index c525904de1..0000000000 --- a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/pre-v19/ng-package/entry-point/write-bundles.transform.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Adapted from the original ng-packagr. - * - * Changes made: - * - Removed bundling altogether. - */ - -import { transformFromPromise } from 'ng-packagr/lib/graph/transform'; -import { NgPackagrOptions } from 'ng-packagr/lib/ng-package/options.di'; - -export const writeBundlesTransform = (_options: NgPackagrOptions) => - transformFromPromise(async (graph) => graph); diff --git a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/pre-v19/ng-package/entry-point/write-package.di.ts b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/pre-v19/ng-package/entry-point/write-package.di.ts deleted file mode 100644 index d742a805b1..0000000000 --- a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/pre-v19/ng-package/entry-point/write-package.di.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Adapted from the original ng-packagr source. - * - * Changes made: - * - Provide our own writePackageTransform function. - */ - -import { - provideTransform, - TransformProvider, -} from 'ng-packagr/lib/graph/transform.di'; -import { WRITE_PACKAGE_TRANSFORM_TOKEN } from 'ng-packagr/lib/ng-package/entry-point/write-package.di'; -import { OPTIONS_TOKEN } from 'ng-packagr/lib/ng-package/options.di'; -import { nxWritePackageTransform } from './write-package.transform'; - -export const WRITE_PACKAGE_TRANSFORM: TransformProvider = provideTransform({ - provide: WRITE_PACKAGE_TRANSFORM_TOKEN, - useFactory: nxWritePackageTransform, - deps: [OPTIONS_TOKEN], -}); diff --git a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/pre-v19/ng-package/entry-point/write-package.transform.ts b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/pre-v19/ng-package/entry-point/write-package.transform.ts deleted file mode 100644 index 2aca0e5df5..0000000000 --- a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-adjustments/pre-v19/ng-package/entry-point/write-package.transform.ts +++ /dev/null @@ -1,424 +0,0 @@ -/** - * Adapted from the original ng-packagr. - * - * Changes made: - * - Change the package.json metadata to only use the ESM2022 output. - */ - -import { logger } from '@nx/devkit'; -import { BuildGraph } from 'ng-packagr/lib/graph/build-graph'; -import { Node } from 'ng-packagr/lib/graph/node'; -import { transformFromPromise } from 'ng-packagr/lib/graph/transform'; -import { NgEntryPoint } from 'ng-packagr/lib/ng-package/entry-point/entry-point'; -import { - EntryPointNode, - fileUrl, - isEntryPointInProgress, - isEntryPoint, - isPackage, - PackageNode, -} from 'ng-packagr/lib/ng-package/nodes'; -import { NgPackagrOptions } from 'ng-packagr/lib/ng-package/options.di'; -import { NgPackage } from 'ng-packagr/lib/ng-package/package'; -import { - copyFile, - exists, - readFile, - rmdir, - stat, - writeFile, -} from 'ng-packagr/lib/utils/fs'; -import { globFiles } from 'ng-packagr/lib/utils/glob'; -import { ensureUnixPath } from 'ng-packagr/lib/utils/path'; -import { AssetPattern } from 'ng-packagr/ng-package.schema'; -import * as path from 'path'; - -export const nxWritePackageTransform = (options: NgPackagrOptions) => - transformFromPromise(async (graph) => { - const entryPoint: EntryPointNode = graph.find(isEntryPointInProgress()); - const ngEntryPoint: NgEntryPoint = entryPoint.data.entryPoint; - const ngPackageNode: PackageNode = graph.find(isPackage); - const ngPackage = ngPackageNode.data; - const { destinationFiles } = entryPoint.data; - - if (!ngEntryPoint.isSecondaryEntryPoint) { - logger.log('Copying assets'); - - try { - await copyAssets(graph, entryPoint, ngPackageNode); - } catch (error) { - throw error; - } - } - - // 6. WRITE PACKAGE.JSON - const relativeUnixFromDestPath = (filePath: string) => - ensureUnixPath(path.relative(ngEntryPoint.destinationPath, filePath)); - - if (!ngEntryPoint.isSecondaryEntryPoint) { - try { - logger.info('Writing package manifest'); - if (!options.watch) { - const primary = ngPackageNode.data.primary; - await writeFile( - path.join(primary.destinationPath, '.npmignore'), - `# Nested package.json's are only needed for development.\n**/package.json` - ); - } - - await writePackageJson( - ngEntryPoint, - ngPackage, - { - module: relativeUnixFromDestPath(destinationFiles.esm2022), - typings: relativeUnixFromDestPath(destinationFiles.declarations), - exports: generatePackageExports(ngEntryPoint, graph), - // webpack v4+ specific flag to enable advanced optimizations and code splitting - sideEffects: ngEntryPoint.packageJson.sideEffects ?? false, - }, - !!options.watch - ); - } catch (error) { - throw error; - } - } else if (ngEntryPoint.isSecondaryEntryPoint) { - if (options.watch) { - // Update the watch version of the primary entry point `package.json` file. - // this is needed because of Webpack's 5 `cachemanagedpaths` - // https://github.com/ng-packagr/ng-packagr/issues/2069 - const primary = ngPackageNode.data.primary; - const packageJsonPath = path.join( - primary.destinationPath, - 'package.json' - ); - - if (await exists(packageJsonPath)) { - const packageJson = JSON.parse( - await readFile(packageJsonPath, { encoding: 'utf8' }) - ); - packageJson.version = generateWatchVersion(); - await writeFile( - path.join(primary.destinationPath, 'package.json'), - JSON.stringify(packageJson, undefined, 2) - ); - } - } - - // Write a package.json in each secondary entry-point - // This is need for esbuild to secondary entry-points in dist correctly. - await writeFile( - path.join(ngEntryPoint.destinationPath, 'package.json'), - JSON.stringify( - { module: relativeUnixFromDestPath(destinationFiles.esm2022) }, - undefined, - 2 - ) - ); - } - - logger.info(`Built ${ngEntryPoint.moduleId}`); - - return graph; - }); - -type AssetEntry = Exclude; - -async function copyAssets( - graph: BuildGraph, - entryPointNode: EntryPointNode, - ngPackageNode: PackageNode -): Promise { - const ngPackage = ngPackageNode.data; - - const globsForceIgnored: string[] = [ - '.gitkeep', - '**/.DS_Store', - '**/Thumbs.db', - `${ngPackage.dest}/**`, - ]; - - const assets: AssetEntry[] = []; - - for (const assetPath of ngPackage.assets) { - let asset: AssetEntry; - if (typeof assetPath === 'object') { - asset = { ...assetPath }; - } else { - const [isDir, isFile] = await stat(path.join(ngPackage.src, assetPath)) - .then((stats) => [stats.isDirectory(), stats.isFile()]) - .catch(() => [false, false]); - if (isDir) { - asset = { glob: '**/*', input: assetPath, output: assetPath }; - } else if (isFile) { - // filenames are their own glob - asset = { - glob: path.basename(assetPath), - input: path.dirname(assetPath), - output: path.dirname(assetPath), - }; - } else { - asset = { glob: assetPath, input: '/', output: '/' }; - } - } - - asset.input = path.join(ngPackage.src, asset.input); - asset.output = path.join(ngPackage.dest, asset.output); - - const isAncestorPath = (target: string, datum: string) => - path.relative(datum, target).startsWith('..'); - if (isAncestorPath(asset.input, ngPackage.src)) { - throw new Error( - 'Cannot read assets from a location outside of the project root.' - ); - } - if (isAncestorPath(asset.output, ngPackage.dest)) { - throw new Error( - 'Cannot write assets to a location outside of the output path.' - ); - } - - assets.push(asset); - } - - for (const asset of assets) { - const filePaths = await globFiles(asset.glob, { - cwd: asset.input, - ignore: [...(asset.ignore ?? []), ...globsForceIgnored], - dot: true, - onlyFiles: true, - followSymbolicLinks: asset.followSymlinks, - }); - for (const filePath of filePaths) { - const fileSrcFullPath = path.join(asset.input, filePath); - const fileDestFullPath = path.join(asset.output, filePath); - const nodeUri = fileUrl(ensureUnixPath(fileSrcFullPath)); - let node = graph.get(nodeUri); - if (!node) { - node = new Node(nodeUri); - graph.put(node); - } - entryPointNode.dependsOn(node); - await copyFile(fileSrcFullPath, fileDestFullPath); - } - } -} - -/** - * Creates and writes a `package.json` file of the entry point used by the `node_module` - * resolution strategies. - * - * #### Example - * - * A consumer of the entry point depends on it by `import {..} from '@my/module/id';`. - * The module id `@my/module/id` will be resolved to the `package.json` file that is written by - * this build step. - * The properties `main`, `module`, `typings` (and so on) in the `package.json` point to the - * flattened JavaScript bundles, type definitions, (...). - * - * @param entryPoint An entry point of an Angular package / library - * @param additionalProperties Additional properties, e.g. binary artefacts (bundle files), to merge into `package.json` - */ -async function writePackageJson( - entryPoint: NgEntryPoint, - pkg: NgPackage, - additionalProperties: { - [key: string]: string | boolean | string[] | ConditionalExport; - }, - isWatchMode: boolean -): Promise { - // set additional properties - const packageJson = { ...entryPoint.packageJson, ...additionalProperties }; - - // read tslib version from `@angular/compiler` so that our tslib - // version at least matches that of angular if we use require('tslib').version - // it will get what installed and not the minimum version nor if it is a `~` or `^` - // this is only required for primary - if (isWatchMode) { - // Needed because of Webpack's 5 `cachemanagedpaths` - // https://github.com/angular/angular-cli/issues/20962 - packageJson.version = generateWatchVersion(); - } - - if ( - !packageJson.peerDependencies?.tslib && - !packageJson.dependencies?.tslib - ) { - const { - peerDependencies: angularPeerDependencies = {}, - dependencies: angularDependencies = {}, - } = require('@angular/compiler/package.json'); - const tsLibVersion = - angularPeerDependencies.tslib || angularDependencies.tslib; - - if (tsLibVersion) { - packageJson.dependencies = { - ...packageJson.dependencies, - tslib: tsLibVersion, - }; - } - } else if (packageJson.peerDependencies?.tslib) { - logger.warn( - `'tslib' is no longer recommended to be used as a 'peerDependencies'. Moving it to 'dependencies'.` - ); - packageJson.dependencies = { - ...(packageJson.dependencies || {}), - tslib: packageJson.peerDependencies.tslib, - }; - - delete packageJson.peerDependencies.tslib; - } - - // Verify non-peerDependencies as they can easily lead to duplicate installs or version conflicts - // in the node_modules folder of an application - const allowedList = pkg.allowedNonPeerDependencies.map( - (value) => new RegExp(value) - ); - try { - checkNonPeerDependencies(packageJson, 'dependencies', allowedList); - } catch (e) { - await rmdir(entryPoint.destinationPath, { recursive: true }); - throw e; - } - - // Removes scripts from package.json after build - if (packageJson.scripts) { - if (pkg.keepLifecycleScripts !== true) { - logger.info( - `Removing scripts section in package.json as it's considered a potential security vulnerability.` - ); - delete packageJson.scripts; - } else { - logger.warn( - `You enabled keepLifecycleScripts explicitly. The scripts section in package.json will be published to npm.` - ); - } - } - - // keep the dist package.json clean - // this will not throw if ngPackage field does not exist - delete packageJson.ngPackage; - - const packageJsonPropertiesToDelete = [ - 'stylelint', - 'prettier', - 'browserslist', - 'devDependencies', - 'jest', - 'workspaces', - 'husky', - ]; - - for (const prop of packageJsonPropertiesToDelete) { - if (prop in packageJson) { - delete packageJson[prop]; - logger.info(`Removing ${prop} section in package.json.`); - } - } - - packageJson.name = entryPoint.moduleId; - await writeFile( - path.join(entryPoint.destinationPath, 'package.json'), - JSON.stringify(packageJson, undefined, 2) - ); -} - -function checkNonPeerDependencies( - packageJson: Record, - property: string, - allowed: RegExp[] -) { - if (!packageJson[property]) { - return; - } - - for (const dep of Object.keys(packageJson[property])) { - if (!allowed.some((regex) => regex.test(dep))) { - logger.warn( - `Distributing npm packages with '${property}' is not recommended. Please consider adding ${dep} to 'peerDependencies' or remove it from '${property}'.` - ); - throw new Error( - `Dependency ${dep} must be explicitly allowed using the "allowedNonPeerDependencies" option.` - ); - } - } -} - -type PackageExports = Record; - -/** - * Type describing the conditional exports descriptor for an entry-point. - * https://nodejs.org/api/packages.html#packages_conditional_exports - */ -type ConditionalExport = { - types?: string; - esm2022?: string; - esm?: string; - default?: string; -}; - -/** - * Generates the `package.json` package exports following APF v13. - * This is supposed to match with: https://github.com/angular/angular/blob/e0667efa6eada64d1fb8b143840689090fc82e52/packages/bazel/src/ng_package/packager.ts#L415. - */ -function generatePackageExports( - { destinationPath, packageJson }: NgEntryPoint, - graph: BuildGraph -): PackageExports { - const exports: PackageExports = packageJson.exports - ? JSON.parse(JSON.stringify(packageJson.exports)) - : {}; - - const insertMappingOrError = ( - subpath: string, - mapping: ConditionalExport - ) => { - exports[subpath] ??= {}; - const subpathExport = exports[subpath]; - - // Go through all conditions that should be inserted. If the condition is already - // manually set of the subpath export, we throw an error. In general, we allow for - // additional conditions to be set. These will always precede the generated ones. - for (const conditionName of Object.keys(mapping)) { - if (subpathExport[conditionName] !== undefined) { - logger.warn( - `Found a conflicting export condition for "${subpath}". The "${conditionName}" ` + - `condition would be overridden by ng-packagr. Please unset it.` - ); - } - - // **Note**: The order of the conditions is preserved even though we are setting - // the conditions once at a time (the latest assignment will be at the end). - subpathExport[conditionName] = mapping[conditionName]; - } - }; - - const relativeUnixFromDestPath = (filePath: string) => - './' + ensureUnixPath(path.relative(destinationPath, filePath)); - - insertMappingOrError('./package.json', { default: './package.json' }); - - const entryPoints = graph.filter(isEntryPoint); - for (const entryPoint of entryPoints) { - const { destinationFiles, isSecondaryEntryPoint } = - entryPoint.data.entryPoint; - const subpath = isSecondaryEntryPoint - ? `./${destinationFiles.directory}` - : '.'; - - insertMappingOrError(subpath, { - types: relativeUnixFromDestPath(destinationFiles.declarations), - esm2022: relativeUnixFromDestPath(destinationFiles.esm2022), - esm: relativeUnixFromDestPath(destinationFiles.esm2022), - default: relativeUnixFromDestPath(destinationFiles.esm2022), - }); - } - - return exports; -} - -/** - * Generates a new version for the package `package.json` when runing in watch mode. - */ -function generateWatchVersion() { - return `0.0.0-watch+${Date.now()}`; -}