Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions packages/angular/build/src/builders/library/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/

import { createRequire } from 'node:module';
import type { BuilderContext } from '@angular-devkit/architect';
import fs from 'node:fs/promises';
import path from 'node:path';
Expand Down Expand Up @@ -49,6 +50,8 @@ export interface PackageJsonData {
export interface NormalizedLibraryOptions {
workspaceRoot: string;
projectRoot: string;
/** The tslib range `@angular/compiler` declares, when it can be resolved from the workspace. */
tslibVersion?: string;
packageName: string;
packageJson: PackageJsonData;
outputPath: string;
Expand Down Expand Up @@ -193,6 +196,7 @@ export async function normalizeLibraryOptions(
projectRoot,
packageName,
packageJson,
tslibVersion: resolveAngularTslibRange(workspaceRoot),
outputPath: resolvedOutputPath,
deleteOutputPath,
packageJsonPath,
Expand Down Expand Up @@ -361,3 +365,26 @@ function normalizeEntryPoints(

return entryPoints;
}

/**
* The tslib range `@angular/compiler` declares, or `undefined` when it cannot be resolved.
*
* Resolved from the workspace so a library gets the range of the Angular it is built against,
* which is what ng-packagr reads too. A missing or unreadable manifest is not an error: the
* output then keeps exactly the dependencies the library declared itself.
*/
function resolveAngularTslibRange(workspaceRoot: string): string | undefined {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since tslib is both backward and forward compatible, using the latest version of the library should be sufficient here.
There's also no guarantee that tslib will even be present in @angular/compiler or that it is resolvable from the workspace root.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main reason for sticking with the @angular/compiler version was to avoid bundling multiple versions of tslib. But since tslib isn't heavily used anymore, using the workspace version works.

I agree we should only inject tslib when it's actually required. Relying on the latest version in the workspace avoids issues if tslib isn't included in @angular/compiler or can't be resolved from the root.

try {
const workspaceRequire = createRequire(path.join(workspaceRoot, 'index.js'));
const { dependencies, peerDependencies } = workspaceRequire(
'@angular/compiler/package.json',
) as {
dependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
};

return peerDependencies?.['tslib'] ?? dependencies?.['tslib'];
} catch {
return undefined;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ export function generatePackageManifests(
options: NormalizedLibraryOptions,
isWatchMode: boolean,
): MemoryOutputFile[] {
const { packageJson: rawPackageJson, keepLifecycleScripts, compilationMode } = options;
const {
packageJson: rawPackageJson,
keepLifecycleScripts,
compilationMode,
tslibVersion,
} = options;

const {
devDependencies: _devDependencies,
Expand Down Expand Up @@ -73,6 +78,19 @@ export function generatePackageManifests(
version: isWatchMode ? `0.0.0-watch+${Date.now()}` : version,
};

// A library compiles with `importHelpers`, so a bundle can carry a live
// `import ... from 'tslib'` while the library's own manifest declares nothing.
// On a hoisted node_modules it resolves anyway; on a strict layout the consumer
// gets ERR_MODULE_NOT_FOUND at runtime. Declare it, as ng-packagr does.
if (
tslibVersion &&
!rawPackageJson.dependencies?.tslib &&
!rawPackageJson.peerDependencies?.tslib &&
!rawPackageJson.optionalDependencies?.tslib
) {
distPackageJson.dependencies = { ...distPackageJson.dependencies, tslib: tslibVersion };
}

// Retain scripts if keepLifecycleScripts is set
if (keepLifecycleScripts && scripts) {
distPackageJson.scripts = scripts;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,4 +307,53 @@ describe('generatePackageManifests', () => {
);
expect(getRootPackageJson(files2).sideEffects).toEqual(['*.css']);
});

it('declares tslib when the library does not', () => {
// A library compiles with importHelpers, so a bundle can import tslib while the
// library's own manifest declares nothing. On a strict node_modules layout the
// consumer then gets ERR_MODULE_NOT_FOUND. ng-packagr adds it; so does this.
const options = createOptions({ tslibVersion: '^2.3.0' });
const result = getRootPackageJson(generatePackageManifests(options, false));

expect(result.dependencies).toEqual({ tslib: '^2.3.0' });
});

it('leaves a declared tslib alone', () => {
const options = createOptions({
tslibVersion: '^2.3.0',
packageJson: { name: 'my-lib', dependencies: { tslib: '^2.0.0' } },
});
const result = getRootPackageJson(generatePackageManifests(options, false));

expect(result.dependencies).toEqual({ tslib: '^2.0.0' });
});

it('does not move a tslib that is declared as a peer', () => {
const options = createOptions({
tslibVersion: '^2.3.0',
packageJson: { name: 'my-lib', peerDependencies: { tslib: '^2.0.0' } },
});
const result = getRootPackageJson(generatePackageManifests(options, false));

expect(result.dependencies).toBeUndefined();
expect(result.peerDependencies).toEqual({ tslib: '^2.0.0' });
});
Comment thread
thekhegay marked this conversation as resolved.

it('does not add a tslib that is declared as optional', () => {
const options = createOptions({
tslibVersion: '^2.3.0',
packageJson: { name: 'my-lib', optionalDependencies: { tslib: '^2.0.0' } },
});
const result = getRootPackageJson(generatePackageManifests(options, false));

expect(result.dependencies).toBeUndefined();
expect(result.optionalDependencies).toEqual({ tslib: '^2.0.0' });
});

it('adds nothing when the range cannot be resolved', () => {
const options = createOptions();
const result = getRootPackageJson(generatePackageManifests(options, false));

expect(result.dependencies).toBeUndefined();
});
});
Loading