Sitelet https://github.com/limitless-angular/limitless-angular/pull/46/files
Skip to content
Merged
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
6 changes: 6 additions & 0 deletions tools/angular-compat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ Supported modes:
- `dist-tag`: npm dist-tag such as `next`. Use this only in
`canaryVersionSets`, because canaries are advisory.

Canary toolchains resolve Angular framework packages from `@angular/core` and
resolve CLI-owned packages, such as `@angular/build` and `@angular/cli`, from
their own npm dist-tags. This keeps the advisory matrix resilient when the
Angular framework and Angular CLI release trains publish prereleases at
different times.

`buildAngularMajor` is the stable major used to build the library artifact. It
must be the newest stable Angular major represented by `consumerVersionSets`.
The test matrix then installs the same tarball into every configured consumer.
Expand Down
101 changes: 77 additions & 24 deletions tools/angular-compat/lib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,24 @@ import semver from 'semver';

const toolRoot = dirname(fileURLToPath(import.meta.url));

const angularFrameworkPackages = new Set([
'@angular/animations',
'@angular/common',
'@angular/compiler',
'@angular/compiler-cli',
'@angular/core',
'@angular/elements',
'@angular/forms',
'@angular/language-service',
'@angular/localize',
'@angular/platform-browser',
'@angular/platform-browser-dynamic',
'@angular/platform-server',
'@angular/router',
'@angular/service-worker',
'@angular/upgrade',
]);

export const workspaceRoot = resolve(toolRoot, '../..');
export const configPath = join(toolRoot, 'config.json');
export const config = readJson(configPath);
Expand Down Expand Up @@ -305,17 +323,23 @@ export function resolveNpmJson(spec, field) {
return JSON.parse(output);
}

export function resolveLatestVersion(spec) {
const version = resolveNpmJson(spec, 'version');
export function isAngularFrameworkPackage(packageName) {
return angularFrameworkPackages.has(packageName);
}

export function resolveLatestVersion(spec, options = {}) {
const npmJson = options.resolveNpmJson ?? resolveNpmJson;
const version = npmJson(spec, 'version');
if (Array.isArray(version)) {
return version.at(-1);
}

return version;
}

export function resolveLatestSatisfying(packageName, range) {
const versions = resolveNpmJson(packageName, 'versions');
export function resolveLatestSatisfying(packageName, range, options = {}) {
const npmJson = options.resolveNpmJson ?? resolveNpmJson;
const versions = npmJson(packageName, 'versions');
const version = semver.maxSatisfying(versions, range);
if (!version) {
throw new Error(`Could not resolve ${packageName} satisfying ${range}`);
Expand All @@ -333,38 +357,60 @@ export function resolveAngularToolchain(versionSetOrMajor, options = {}) {
mode: 'latest',
}
: normalizeVersionSet(versionSetOrMajor);
const angularVersion = resolveAngularVersion(versionSet);
const resolverOptions = {
resolveNpmJson: options.resolveNpmJson ?? resolveNpmJson,
};
const angularVersion = resolveAngularVersion(versionSet, resolverOptions);
const angularMajor = semver.major(angularVersion);
const compilerCliVersion = resolveAngularPackageVersion(
'@angular/compiler-cli',
versionSet,
angularVersion,
resolverOptions,
);
const compilerCliPeers = resolveNpmJson(
const compilerCliPeers = resolverOptions.resolveNpmJson(
`@angular/compiler-cli@${compilerCliVersion}`,
'peerDependencies',
);
const corePeers = resolveNpmJson(
const corePeers = resolverOptions.resolveNpmJson(
`@angular/core@${angularVersion}`,
'peerDependencies',
);
const angularBuildVersion = options.includeCli
? resolveAngularPackageVersion('@angular/build', versionSet, angularVersion)
? resolveAngularPackageVersion(
'@angular/build',
versionSet,
angularVersion,
resolverOptions,
)
: undefined;
const angularBuildPeers = angularBuildVersion
? resolveNpmJson(
? resolverOptions.resolveNpmJson(
`@angular/build@${angularBuildVersion}`,
'peerDependencies',
)
: undefined;
const cliVersion = options.includeCli
? resolveAngularPackageVersion('@angular/cli', versionSet)
? resolveAngularPackageVersion(
'@angular/cli',
versionSet,
undefined,
resolverOptions,
)
: undefined;
const ngPackagrVersion = options.includeNgPackagr
? resolveAngularPackageVersion('ng-packagr', versionSet)
? resolveAngularPackageVersion(
'ng-packagr',
versionSet,
undefined,
resolverOptions,
)
: undefined;
const ngPackagrPeers = ngPackagrVersion
? resolveNpmJson(`ng-packagr@${ngPackagrVersion}`, 'peerDependencies')
? resolverOptions.resolveNpmJson(
`ng-packagr@${ngPackagrVersion}`,
'peerDependencies',
)
: undefined;
const typescriptRange = [
compilerCliPeers.typescript,
Expand All @@ -376,9 +422,10 @@ export function resolveAngularToolchain(versionSetOrMajor, options = {}) {
const typescriptVersion = resolveLatestSatisfying(
'typescript',
typescriptRange,
resolverOptions,
);
const zoneVersion = corePeers['zone.js']
? resolveLatestSatisfying('zone.js', corePeers['zone.js'])
? resolveLatestSatisfying('zone.js', corePeers['zone.js'], resolverOptions)
: undefined;

return {
Expand All @@ -395,39 +442,45 @@ export function resolveAngularToolchain(versionSetOrMajor, options = {}) {
};
}

function resolveAngularVersion(versionSet) {
function resolveAngularVersion(versionSet, options = {}) {
if (versionSet.mode === 'dist-tag') {
return resolveLatestVersion(`@angular/core@${versionSet.npmTag}`);
return resolveLatestVersion(`@angular/core@${versionSet.npmTag}`, options);
}

return resolveAngularPackageVersion('@angular/core', versionSet);
return resolveAngularPackageVersion(
'@angular/core',
versionSet,
undefined,
options,
);
}

function resolveAngularPackageVersion(
export function resolveAngularPackageVersion(
packageName,
versionSet,
exactAngularVersion,
options = {},
) {
if (
exactAngularVersion &&
packageName.startsWith('@angular/') &&
packageName !== '@angular/cli'
) {
if (exactAngularVersion && isAngularFrameworkPackage(packageName)) {
return exactAngularVersion;
}

if (versionSet.mode === 'dist-tag') {
return resolveLatestVersion(`${packageName}@${versionSet.npmTag}`);
return resolveLatestVersion(`${packageName}@${versionSet.npmTag}`, options);
}

if (versionSet.mode === 'floor') {
return resolveLatestSatisfying(
packageName,
`>=${versionSet.angularMajor}.0.0 <${versionSet.angularMajor}.1.0`,
options,
);
}

return resolveLatestVersion(`${packageName}@${versionSet.angularMajor}`);
return resolveLatestVersion(
`${packageName}@${versionSet.angularMajor}`,
options,
);
}

export function getVersionSetLabel(versionSet) {
Expand Down
93 changes: 93 additions & 0 deletions tools/angular-compat/lib.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';

import {
isAngularFrameworkPackage,
resolveAngularPackageVersion,
resolveAngularToolchain,
} from './lib.mjs';

test('Angular framework packages reuse the exact resolved framework version', () => {
const { resolveNpmJson } = createNpmJsonResolver();

assert.equal(isAngularFrameworkPackage('@angular/compiler-cli'), true);
assert.equal(
resolveAngularPackageVersion(
'@angular/compiler-cli',
{ id: 'angular-next', mode: 'dist-tag', npmTag: 'next' },
'22.1.0-next.0',
{ resolveNpmJson },
),
'22.1.0-next.0',
);
});

test('Angular CLI-owned packages resolve from their own dist-tag', () => {
const { calls, resolveNpmJson } = createNpmJsonResolver({
'@angular/build@next version': '22.0.0',
});

assert.equal(isAngularFrameworkPackage('@angular/build'), false);
assert.equal(
resolveAngularPackageVersion(
'@angular/build',
{ id: 'angular-next', mode: 'dist-tag', npmTag: 'next' },
'22.1.0-next.0',
{ resolveNpmJson },
),
'22.0.0',
);
assert.deepEqual(calls, [{ field: 'version', spec: '@angular/build@next' }]);
});

test('canary toolchain allows framework and CLI next tags to move independently', () => {
const { calls, resolveNpmJson } = createNpmJsonResolver({
'@angular/core@next version': '22.1.0-next.0',
'@angular/compiler-cli@22.1.0-next.0 peerDependencies': {
typescript: '>=6.0 <6.2',
},
'@angular/core@22.1.0-next.0 peerDependencies': {
'zone.js': '^0.16.0',
},
'@angular/build@next version': '22.0.0',
'@angular/build@22.0.0 peerDependencies': {
typescript: '>=6.0 <6.1',
},
'@angular/cli@next version': '22.0.0',
'typescript versions': ['6.0.0', '6.0.1', '6.1.0'],
'zone.js versions': ['0.15.0', '0.16.0'],
});

const toolchain = resolveAngularToolchain(
{ id: 'angular-next', mode: 'dist-tag', npmTag: 'next' },
{ includeCli: true, resolveNpmJson },
);

assert.equal(toolchain.angularVersion, '22.1.0-next.0');
assert.equal(toolchain.compilerCliVersion, '22.1.0-next.0');
assert.equal(toolchain.angularBuildVersion, '22.0.0');
assert.equal(toolchain.cliVersion, '22.0.0');
assert.equal(toolchain.typescriptVersion, '6.0.1');
assert.equal(toolchain.zoneVersion, '0.16.0');
assert.equal(
calls.some(({ spec }) => spec === '@angular/build@22.1.0-next.0'),
false,
);
});

function createNpmJsonResolver(entries = {}) {
const calls = [];

return {
calls,
resolveNpmJson(spec, field) {
calls.push({ field, spec });
const key = `${spec} ${field}`;
if (!Object.hasOwn(entries, key)) {
throw new Error(`Unexpected npm metadata request: ${key}`);
}

return entries[key];
},
};
}
Loading