angular-compat-matrix 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,131 @@
1
+ import { copyFileSync, writeFileSync } from 'node:fs';
2
+ import { basename, join } from 'node:path';
3
+ import semver from 'semver';
4
+ import { readJson, writeJson } from "./app.js";
5
+ import { errorLines, exec, npm, quietEnv } from "./exec.js";
6
+ import { ensureTool, PNPM_VERSION, pnpmEntry } from "./tools.js";
7
+ export function addDependencies(appDir, deps, extra = {}) {
8
+ const path = join(appDir, 'package.json');
9
+ const manifest = readJson(path);
10
+ manifest.dependencies = { ...(manifest.dependencies ?? {}), ...deps };
11
+ Object.assign(manifest, extra);
12
+ writeJson(path, manifest);
13
+ }
14
+ export async function installLibrary(request) {
15
+ const tgz = basename(request.tarball);
16
+ copyFileSync(request.tarball, join(request.appDir, tgz));
17
+ const deps = { ...request.plan.installs, [request.libraryName]: `file:${tgz}` };
18
+ return request.pm === 'npm' ? installWithNpm(request, deps) : installWithPnpm(request, deps, tgz);
19
+ }
20
+ /**
21
+ * npm 11 fails on any peer conflict and can't ignore just one. So: install without peer
22
+ * resolution, with the peers added explicitly, then let `npm ls` report every invalid peer.
23
+ */
24
+ async function installWithNpm(request, deps) {
25
+ addDependencies(request.appDir, deps);
26
+ const args = ['install', '--legacy-peer-deps', '--no-audit', '--no-fund'];
27
+ if (request.offline)
28
+ args.push('--offline');
29
+ const result = await npm(args, { cwd: request.appDir, timeoutMs: 15 * 60_000 });
30
+ if (result.code !== 0) {
31
+ return { pass: false, message: 'npm install failed.', errorLines: errorLines(result.output), ignored: [] };
32
+ }
33
+ const ls = await npm(['ls', '--all'], { cwd: request.appDir });
34
+ const problems = parseNpmLs(ls.output);
35
+ return judgeProblems(request, problems);
36
+ }
37
+ /** Reads `npm ls --all` text output: `@angular/core@21.2.25 deduped invalid: "^22.0.0" from node_modules/@angular/cdk`. */
38
+ export function parseNpmLs(output) {
39
+ const problems = [];
40
+ for (const raw of output.split(/\r?\n/)) {
41
+ const line = raw.replace(/^[\s|`+\\-]+/, '').trim();
42
+ const invalid = /^(@?[^@\s]+)@(\S+)(?: deduped)? invalid: (.+)$/.exec(line);
43
+ if (invalid) {
44
+ for (const want of invalid[3].matchAll(/"([^"]+)" from (\S+?)(?:,|$)/g)) {
45
+ problems.push({ name: invalid[1], installed: invalid[2], wanted: want[1], by: want[2].replace(/^.*node_modules\//, ''), line });
46
+ }
47
+ continue;
48
+ }
49
+ const unmet = /UNMET (?:OPTIONAL )?(?:PEER )?DEPENDENCY (@?[^@\s]+)@(\S+)/.exec(line);
50
+ if (unmet && !/OPTIONAL/.test(line))
51
+ problems.push({ name: unmet[1], wanted: unmet[2], line });
52
+ }
53
+ return problems;
54
+ }
55
+ function judgeProblems(request, problems) {
56
+ // Only conflicts about the package under test and the peers installed for it count;
57
+ // anything else in the tree is the generated app's own business.
58
+ const ours = new Set([request.libraryName, ...Object.keys(request.plan.installs)]);
59
+ const ourPeers = new Set([...Object.keys(request.plan.installs), ...Object.keys(request.plan.checked)]);
60
+ const relevant = problems.filter((p) => (p.by ? ours.has(p.by) : ourPeers.has(p.name)));
61
+ const outside = request.plan.claimed === false;
62
+ const ignored = relevant.filter((p) => outside && p.name.startsWith('@angular/') && p.by === request.libraryName);
63
+ const failing = relevant.filter((p) => !ignored.includes(p));
64
+ const describe = (p) => `${p.by ?? '?'} wants ${p.name}@"${p.wanted}"${p.installed ? `, found ${p.installed}` : ', missing'}`;
65
+ if (failing.length) {
66
+ return {
67
+ pass: false,
68
+ message: `Peer conflict: ${describe(failing[0])}${failing.length > 1 ? ` (+${failing.length - 1} more)` : ''}.`,
69
+ errorLines: failing.map((p) => p.line),
70
+ ignored: ignored.map(describe),
71
+ };
72
+ }
73
+ return {
74
+ pass: true,
75
+ message: ignored.length ? `Installed; ignored the expected conflict: ${ignored.map(describe).join('; ')}.` : 'Installed.',
76
+ errorLines: [],
77
+ ignored: ignored.map(describe),
78
+ };
79
+ }
80
+ /**
81
+ * pnpm only warns about peers by default, so strictPeerDependencies is on, with allowedVersions
82
+ * for the one expected conflict. strictDepBuilds is off: otherwise pnpm 12 fails every install
83
+ * on Angular's own esbuild, lmdb and @parcel/watcher scripts. The library's own scripts may run.
84
+ */
85
+ async function installWithPnpm(request, deps, tgz) {
86
+ addDependencies(request.appDir, deps, { packageManager: `pnpm@${PNPM_VERSION}` });
87
+ const allowed = {};
88
+ if (request.plan.claimed === false) {
89
+ for (const miss of request.plan.misses) {
90
+ const major = semver.valid(miss.version) ? String(semver.major(miss.version)) : String(request.angularMajor);
91
+ allowed[`${request.libraryName}>${miss.name}`] = major;
92
+ }
93
+ }
94
+ writeFileSync(join(request.appDir, 'pnpm-workspace.yaml'), pnpmWorkspaceYaml(request.libraryName, tgz, allowed));
95
+ const pnpm = await ensureTool(request.cacheDir, 'pnpm', PNPM_VERSION, request.offline);
96
+ const args = [pnpmEntry(pnpm), 'install', '--no-frozen-lockfile'];
97
+ if (request.offline)
98
+ args.push('--offline');
99
+ const result = await exec(process.execPath, args, { cwd: request.appDir, env: quietEnv(), timeoutMs: 15 * 60_000 });
100
+ const ignored = Object.entries(allowed).map(([key, major]) => `${key} allowed at ${major}.x`);
101
+ if (result.code !== 0) {
102
+ const peer = /ERR_PNPM_PEER_DEP_ISSUES|unmet peer|Issues with peer dependencies/i.test(result.output);
103
+ return {
104
+ pass: false,
105
+ message: peer ? 'Peer conflict (pnpm strict-peer-dependencies).' : 'pnpm install failed.',
106
+ errorLines: errorLines(result.output),
107
+ ignored,
108
+ };
109
+ }
110
+ return {
111
+ pass: true,
112
+ message: ignored.length ? `Installed; allowed the expected conflict: ${ignored.join('; ')}.` : 'Installed.',
113
+ errorLines: [],
114
+ ignored,
115
+ };
116
+ }
117
+ export function pnpmWorkspaceYaml(libraryName, tgz, allowed) {
118
+ const lines = [
119
+ 'strictPeerDependencies: true',
120
+ 'strictDepBuilds: false',
121
+ 'allowBuilds:',
122
+ // pnpm keys a tarball dependency by its full spec; the bare name doesn't match it.
123
+ ` ${JSON.stringify(`${libraryName}@file:${tgz}`)}: true`,
124
+ ];
125
+ if (Object.keys(allowed).length) {
126
+ lines.push('peerDependencyRules:', ' allowedVersions:');
127
+ for (const [key, range] of Object.entries(allowed))
128
+ lines.push(` ${JSON.stringify(key)}: ${JSON.stringify(range)}`);
129
+ }
130
+ return lines.join('\n') + '\n';
131
+ }
@@ -0,0 +1,184 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { basename, dirname, join, resolve } from 'node:path';
4
+ import * as tar from 'tar';
5
+ import { errorLines, exec, npm, quietEnv } from "./exec.js";
6
+ export class LibraryError extends Error {
7
+ lines;
8
+ constructor(message, lines = []) {
9
+ super(message);
10
+ this.lines = lines;
11
+ }
12
+ }
13
+ function readJson(path) {
14
+ return JSON.parse(readFileSync(path, 'utf8'));
15
+ }
16
+ /** `dest` from an ng-package.json (or a package.json `ngPackage` field): the same for the CLI, ng-packagr and Nx. */
17
+ export function ngPackageDest(ngPackagePath) {
18
+ const config = basename(ngPackagePath) === 'package.json'
19
+ ? (readJson(ngPackagePath).ngPackage ?? {})
20
+ : readJson(ngPackagePath);
21
+ return resolve(dirname(ngPackagePath), config.dest ?? 'dist');
22
+ }
23
+ function resolveBin(cwd, pkg, bin) {
24
+ const require = createRequire(join(cwd, 'noop.js'));
25
+ let manifestPath;
26
+ try {
27
+ manifestPath = require.resolve(`${pkg}/package.json`);
28
+ }
29
+ catch {
30
+ throw new LibraryError(`${pkg} isn't installed in ${cwd}. Install the workspace's dependencies first.`);
31
+ }
32
+ const manifest = readJson(manifestPath);
33
+ const relativeBin = typeof manifest.bin === 'string' ? manifest.bin : manifest.bin?.[bin];
34
+ if (!relativeBin)
35
+ throw new LibraryError(`${pkg} has no "${bin}" command.`);
36
+ return join(dirname(manifestPath), relativeBin);
37
+ }
38
+ export function detectBuild(options) {
39
+ const { cwd } = options;
40
+ const shell = (command) => () => exec(command, [], { cwd, shell: true });
41
+ const angularJson = join(cwd, 'angular.json');
42
+ if (existsSync(angularJson)) {
43
+ const workspace = readJson(angularJson);
44
+ const libraries = Object.entries(workspace.projects).filter(([, p]) => p.projectType === 'library');
45
+ if (!libraries.length)
46
+ throw new LibraryError('angular.json has no library project.');
47
+ let name = options.project;
48
+ if (!name) {
49
+ if (libraries.length > 1) {
50
+ throw new LibraryError(`This workspace has ${libraries.length} libraries (${libraries.map(([n]) => n).join(', ')}). Pick one with --project.`);
51
+ }
52
+ name = libraries[0][0];
53
+ }
54
+ const project = workspace.projects[name];
55
+ if (!project || project.projectType !== 'library')
56
+ throw new LibraryError(`No library project named "${name}" in angular.json.`);
57
+ const ngPackage = project.architect?.build?.options?.project ?? join(project.root ?? '', 'ng-package.json');
58
+ const dest = ngPackageDest(resolve(cwd, ngPackage));
59
+ if (options.build)
60
+ return { kind: 'angular-cli', label: options.build, dest, run: shell(options.build) };
61
+ return {
62
+ kind: 'angular-cli',
63
+ label: `ng build ${name} --configuration production`,
64
+ dest,
65
+ run: () => exec(process.execPath, [resolveBin(cwd, '@angular/cli', 'ng'), 'build', name, '--configuration', 'production'], {
66
+ cwd,
67
+ env: quietEnv(),
68
+ }),
69
+ };
70
+ }
71
+ const ngPackage = ['ng-package.json', 'package.json'].map((f) => join(cwd, f)).find((path) => {
72
+ if (!existsSync(path))
73
+ return false;
74
+ return basename(path) === 'ng-package.json' || Boolean(readJson(path).ngPackage);
75
+ });
76
+ if (ngPackage) {
77
+ const dest = ngPackageDest(ngPackage);
78
+ if (options.build)
79
+ return { kind: 'ng-packagr', label: options.build, dest, run: shell(options.build) };
80
+ const manifest = existsSync(join(cwd, 'package.json')) ? readJson(join(cwd, 'package.json')) : undefined;
81
+ if (manifest?.scripts?.build) {
82
+ return { kind: 'ng-packagr', label: 'npm run build', dest, run: () => npm(['run', 'build'], { cwd }) };
83
+ }
84
+ return {
85
+ kind: 'ng-packagr',
86
+ label: 'ng-packagr -p ng-package.json',
87
+ dest,
88
+ run: () => exec(process.execPath, [resolveBin(cwd, 'ng-packagr', 'ng-packagr'), '-p', ngPackage], { cwd }),
89
+ };
90
+ }
91
+ if (existsSync(join(cwd, 'nx.json'))) {
92
+ throw new LibraryError('Nx workspaces are planned for v0.2. For now, run ng-matrix from the library folder (the one with ng-package.json) with --build "npx nx build <lib>".');
93
+ }
94
+ throw new LibraryError('No Angular library here: expected angular.json with a library project, or ng-package.json. Or pass --tarball / --package.');
95
+ }
96
+ /**
97
+ * Full compilation output must never be published: ng-packagr guards that with a `prepublishOnly`
98
+ * script, which `npm pack` doesn't run. So ng-matrix checks for it itself.
99
+ */
100
+ export function fullCompilationEvidence(dir) {
101
+ const manifestPath = join(dir, 'package.json');
102
+ if (existsSync(manifestPath)) {
103
+ const guard = readJson(manifestPath).scripts?.prepublishOnly ?? '';
104
+ if (/full compilation mode/i.test(guard))
105
+ return 'its package.json has ng-packagr\'s "full compilation mode" prepublishOnly guard';
106
+ }
107
+ const define = /ɵɵdefine(Component|Directive|Pipe|NgModule)\(/;
108
+ const stack = [dir];
109
+ while (stack.length) {
110
+ const current = stack.pop();
111
+ for (const entry of readdirSync(current)) {
112
+ if (entry === 'node_modules')
113
+ continue;
114
+ const path = join(current, entry);
115
+ if (statSync(path).isDirectory())
116
+ stack.push(path);
117
+ else if (/\.m?js$/.test(entry)) {
118
+ const match = define.exec(readFileSync(path, 'utf8'));
119
+ if (match)
120
+ return `${path.slice(dir.length + 1)} calls ɵɵdefine${match[1]}, which only full compilation emits`;
121
+ }
122
+ }
123
+ }
124
+ return undefined;
125
+ }
126
+ async function pack(source, into) {
127
+ const result = await npm(['pack', source, '--pack-destination', into, '--json', '--ignore-scripts'], { cwd: into });
128
+ if (result.code !== 0)
129
+ throw new LibraryError(`npm pack ${source} failed.`, errorLines(result.output));
130
+ const json = result.output.slice(result.output.indexOf('['));
131
+ const [info] = JSON.parse(json);
132
+ // Scoped packages: npm reports "@scope/name-1.0.0.tgz" but writes "scope-name-1.0.0.tgz".
133
+ return join(into, info.filename.replace(/^@/, '').replace('/', '-'));
134
+ }
135
+ export async function extract(tarball, into) {
136
+ mkdirSync(into, { recursive: true });
137
+ await tar.x({ file: tarball, cwd: into });
138
+ const dir = join(into, 'package');
139
+ if (!existsSync(join(dir, 'package.json')))
140
+ throw new LibraryError(`${tarball} has no package/package.json.`);
141
+ return dir;
142
+ }
143
+ export async function prepareLibrary(options, workDir, log) {
144
+ const packDir = join(workDir, 'pack');
145
+ mkdirSync(packDir, { recursive: true });
146
+ let tarball;
147
+ let source;
148
+ if (options.tarball) {
149
+ tarball = resolve(options.cwd, options.tarball);
150
+ if (!existsSync(tarball))
151
+ throw new LibraryError(`No tarball at ${tarball}.`);
152
+ source = `tarball ${tarball}`;
153
+ }
154
+ else if (options.packageSpec) {
155
+ log(`Downloading ${options.packageSpec}…`);
156
+ tarball = await pack(options.packageSpec, packDir);
157
+ source = `${options.packageSpec} from the registry`;
158
+ }
159
+ else {
160
+ const plan = detectBuild(options);
161
+ if (!options.skipBuild) {
162
+ log(`Building the library: ${plan.label}`);
163
+ const built = await plan.run();
164
+ if (built.code !== 0)
165
+ throw new LibraryError(`The library build failed (${plan.label}).`, errorLines(built.output));
166
+ }
167
+ if (!existsSync(join(plan.dest, 'package.json'))) {
168
+ throw new LibraryError(`No package.json in the build output ${plan.dest}. Is ng-package.json's "dest" right?`);
169
+ }
170
+ const full = fullCompilationEvidence(plan.dest);
171
+ if (full) {
172
+ throw new LibraryError(`Refusing to pack ${plan.dest}: it was built in full compilation mode (${full}). Libraries must be published in partial mode; build with the production configuration or set "compilationMode": "partial".`);
173
+ }
174
+ tarball = await pack(plan.dest, packDir);
175
+ source = `${plan.label}, then npm pack`;
176
+ }
177
+ const dir = await extract(tarball, join(packDir, 'contents'));
178
+ if (options.tarball || options.packageSpec) {
179
+ const full = fullCompilationEvidence(dir);
180
+ if (full)
181
+ throw new LibraryError(`This package was built in full compilation mode (${full}); Angular apps can't use it reliably.`);
182
+ }
183
+ return { manifest: readJson(join(dir, 'package.json')), tarball, dir, source };
184
+ }
package/dist/model.js ADDED
@@ -0,0 +1,12 @@
1
+ export const CHECKS = ['peer', 'install', 'partial', 'aot', 'strict', 'tests'];
2
+ export const CHECK_LABELS = {
3
+ peer: 'peer range',
4
+ install: 'install',
5
+ partial: 'partial',
6
+ aot: 'AOT build',
7
+ strict: 'strict types',
8
+ tests: 'tests',
9
+ };
10
+ export function result(status, message = '', extra = {}) {
11
+ return { status, message, errorLines: [], durationMs: 0, ...extra };
12
+ }
@@ -0,0 +1,90 @@
1
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { join, relative } from 'node:path';
3
+ import semver from 'semver';
4
+ const DECLARE = /ɵɵngDeclare(\w+)\(\s*\{/g;
5
+ export function scanSource(source, file) {
6
+ const found = [];
7
+ for (const match of source.matchAll(DECLARE)) {
8
+ // The fields come first in every declaration, well within this window.
9
+ const body = source.slice(match.index + match[0].length, match.index + match[0].length + 800);
10
+ const minVersion = /minVersion:\s*["']([^"']+)["']/.exec(body)?.[1];
11
+ const version = /\bversion:\s*["']([^"']+)["']/.exec(body)?.[1];
12
+ const type = /\btype:\s*([\w$]+)/.exec(body)?.[1] ?? '?';
13
+ if (minVersion && version)
14
+ found.push({ kind: match[1], type, minVersion, version, file });
15
+ }
16
+ return found;
17
+ }
18
+ export function scanPackage(dir) {
19
+ const found = [];
20
+ const walk = (current) => {
21
+ for (const entry of readdirSync(current)) {
22
+ if (entry === 'node_modules')
23
+ continue;
24
+ const path = join(current, entry);
25
+ if (statSync(path).isDirectory())
26
+ walk(path);
27
+ else if (/\.(mjs|js|cjs)$/.test(entry))
28
+ found.push(...scanSource(readFileSync(path, 'utf8'), relative(dir, path)));
29
+ }
30
+ };
31
+ walk(dir);
32
+ return found;
33
+ }
34
+ export function summarize(declarations) {
35
+ let highest;
36
+ let compiledWith;
37
+ for (const declaration of declarations) {
38
+ const min = semver.coerce(declaration.minVersion);
39
+ if (min && (!highest || semver.gt(min, semver.coerce(highest.minVersion))))
40
+ highest = declaration;
41
+ // Angular's own source builds say 0.0.0-PLACEHOLDER; that tells us nothing.
42
+ if (semver.valid(declaration.version) && !declaration.version.startsWith('0.0.0')) {
43
+ if (!compiledWith || semver.gt(declaration.version, compiledWith))
44
+ compiledWith = declaration.version;
45
+ }
46
+ }
47
+ return { declarations: declarations.length, highest, compiledWith };
48
+ }
49
+ export function judgePartial(summary, angularVersion) {
50
+ const app = semver.coerce(angularVersion);
51
+ if (!summary.highest) {
52
+ return { pass: true, advisory: false, message: 'No Angular declarations in the package.' };
53
+ }
54
+ const { highest } = summary;
55
+ const where = `${highest.type} (ɵɵngDeclare${highest.kind}, ${highest.file})`;
56
+ if (semver.lt(app, semver.coerce(highest.minVersion))) {
57
+ return {
58
+ pass: false,
59
+ advisory: false,
60
+ message: `Needs Angular ${highest.minVersion} or later: highest minVersion ${highest.minVersion}, in ${where}.`,
61
+ };
62
+ }
63
+ const advisory = Boolean(summary.compiledWith && semver.major(summary.compiledWith) > app.major);
64
+ return {
65
+ pass: true,
66
+ advisory,
67
+ message: advisory
68
+ ? `Highest minVersion ${highest.minVersion} allows it, but the package was compiled with Angular ${summary.compiledWith} and minVersion has been too low before, so only the build can tell.`
69
+ : `Highest minVersion ${highest.minVersion}, in ${where}.`,
70
+ };
71
+ }
72
+ /** The `@angular/*` packages the published JavaScript imports (`@angular/common/http` counts as `@angular/common`). */
73
+ export function angularImports(dir) {
74
+ const found = new Set();
75
+ const specifier = /(?:\bfrom|\bimport)\s*\(?\s*["'](@angular\/[\w.-]+)(?:\/[^"']*)?["']/g;
76
+ const walk = (current) => {
77
+ for (const entry of readdirSync(current)) {
78
+ if (entry === 'node_modules')
79
+ continue;
80
+ const path = join(current, entry);
81
+ if (statSync(path).isDirectory())
82
+ walk(path);
83
+ else if (/\.(mjs|js|cjs)$/.test(entry))
84
+ for (const match of readFileSync(path, 'utf8').matchAll(specifier))
85
+ found.add(match[1]);
86
+ }
87
+ };
88
+ walk(dir);
89
+ return [...found].sort();
90
+ }
package/dist/peers.js ADDED
@@ -0,0 +1,79 @@
1
+ import semver from 'semver';
2
+ import { newestSatisfying } from "./versions.js";
3
+ /** Packages released in lockstep with `@angular/core`: same version number, always. */
4
+ export const FRAMEWORK = new Set([
5
+ '@angular/animations',
6
+ '@angular/common',
7
+ '@angular/compiler',
8
+ '@angular/compiler-cli',
9
+ '@angular/core',
10
+ '@angular/elements',
11
+ '@angular/forms',
12
+ '@angular/language-service',
13
+ '@angular/localize',
14
+ '@angular/platform-browser',
15
+ '@angular/platform-browser-dynamic',
16
+ '@angular/platform-server',
17
+ '@angular/router',
18
+ '@angular/service-worker',
19
+ '@angular/upgrade',
20
+ ]);
21
+ /** Peers a generated app already has at the right version, so they're never installed again. */
22
+ const APP_PROVIDED = new Set(['rxjs', 'tslib', 'zone.js', 'typescript']);
23
+ function isOptional(manifest, name) {
24
+ return Boolean(manifest.peerDependenciesMeta?.[name]?.optional);
25
+ }
26
+ /**
27
+ * The newest version of a non-framework peer (Angular Material, the CDK, a third-party package)
28
+ * that a real app on this Angular would use: one whose own `@angular/*` peers accept it,
29
+ * preferring versions the library's range allows.
30
+ */
31
+ export function pickPeerVersion(packument, libraryRange, angularVersion) {
32
+ const pre = Boolean(semver.prerelease(angularVersion));
33
+ const candidates = Object.keys(packument.versions)
34
+ .filter((v) => semver.valid(v) && (pre || !semver.prerelease(v)))
35
+ .sort(semver.rcompare);
36
+ const acceptsAngular = (v) => {
37
+ const peers = packument.versions[v].peerDependencies ?? {};
38
+ const framework = Object.entries(peers).filter(([name]) => FRAMEWORK.has(name));
39
+ return framework.every(([, range]) => semver.satisfies(angularVersion, range, { includePrerelease: true }));
40
+ };
41
+ const inRange = (v) => semver.satisfies(v, libraryRange, { includePrerelease: true });
42
+ const hasFrameworkPeer = (v) => Object.keys(packument.versions[v].peerDependencies ?? {}).some((name) => FRAMEWORK.has(name));
43
+ // Packages that don't depend on Angular (say, a date library) just follow the library's range.
44
+ if (!candidates.some(hasFrameworkPeer))
45
+ return candidates.find(inRange) ?? newestSatisfying(packument, '*');
46
+ return (candidates.find((v) => acceptsAngular(v) && inRange(v)) ??
47
+ candidates.find(acceptsAngular) ??
48
+ candidates.find(inRange));
49
+ }
50
+ export async function planPeers(manifest, angularVersion, registry) {
51
+ const peers = manifest.peerDependencies ?? {};
52
+ const installs = {};
53
+ const checked = {};
54
+ const misses = [];
55
+ let angularPeers = 0;
56
+ for (const [name, range] of Object.entries(peers)) {
57
+ let version;
58
+ if (FRAMEWORK.has(name)) {
59
+ version = angularVersion;
60
+ installs[name] = angularVersion;
61
+ }
62
+ else if (!APP_PROVIDED.has(name)) {
63
+ version = pickPeerVersion(await registry.get(name), range, angularVersion);
64
+ if (version)
65
+ installs[name] = version;
66
+ }
67
+ // Optional peers are installed (the smoke app imports every entry point, and some entry point
68
+ // needs them, as angular-calendar's date-fns adapter needs date-fns) but claim nothing.
69
+ if (!name.startsWith('@angular/') || isOptional(manifest, name))
70
+ continue;
71
+ angularPeers++;
72
+ const against = version ?? 'none';
73
+ checked[name] = against;
74
+ if (!version || !semver.satisfies(version, range, { includePrerelease: true })) {
75
+ misses.push({ name, range, version: against });
76
+ }
77
+ }
78
+ return { claimed: angularPeers ? misses.length === 0 : null, misses, installs, checked };
79
+ }
@@ -0,0 +1,85 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ const DAY = 24 * 60 * 60 * 1000;
4
+ const FULL_DOCUMENT = new Set(['@angular/core']);
5
+ export class Registry {
6
+ memory = new Map();
7
+ options;
8
+ constructor(options) {
9
+ this.options = options;
10
+ }
11
+ get(name) {
12
+ let pending = this.memory.get(name);
13
+ if (!pending) {
14
+ pending = this.load(name);
15
+ this.memory.set(name, pending);
16
+ }
17
+ return pending;
18
+ }
19
+ async load(name) {
20
+ const file = `${encodeURIComponent(name)}.json`;
21
+ if (this.options.recordedDir) {
22
+ const path = join(this.options.recordedDir, file);
23
+ if (!existsSync(path))
24
+ throw new Error(`No recorded registry answer for ${name} (${path})`);
25
+ return JSON.parse(readFileSync(path, 'utf8'));
26
+ }
27
+ const dir = join(this.options.cacheDir, 'registry');
28
+ const path = join(dir, file);
29
+ const cached = existsSync(path)
30
+ ? JSON.parse(readFileSync(path, 'utf8'))
31
+ : undefined;
32
+ const maxAge = this.options.maxAgeMs ?? DAY;
33
+ if (cached && (this.options.offline || Date.now() - cached.fetchedAt < maxAge))
34
+ return cached.packument;
35
+ if (this.options.offline)
36
+ throw new Error(`--offline: ${name} isn't in the registry cache yet. Run once online first.`);
37
+ const base = (this.options.registryUrl ?? process.env.npm_config_registry ?? 'https://registry.npmjs.org/').replace(/\/?$/, '/');
38
+ const url = base + name.replace('/', '%2f');
39
+ // Only @angular/core's publish dates are needed (to tell which majors are supported); everything
40
+ // else comes from the abbreviated document, which is much smaller for packages with many versions.
41
+ const accept = FULL_DOCUMENT.has(name)
42
+ ? 'application/json'
43
+ : 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8';
44
+ let response;
45
+ try {
46
+ response = await fetch(url, { headers: { accept } });
47
+ }
48
+ catch (error) {
49
+ if (cached)
50
+ return cached.packument;
51
+ throw new Error(`Couldn't reach the npm registry for ${name}: ${error.message}`);
52
+ }
53
+ if (!response.ok) {
54
+ if (cached)
55
+ return cached.packument;
56
+ throw new Error(`The npm registry answered ${response.status} for ${name}`);
57
+ }
58
+ const packument = trim((await response.json()));
59
+ mkdirSync(dir, { recursive: true });
60
+ const tmp = `${path}.${process.pid}.tmp`;
61
+ writeFileSync(tmp, JSON.stringify({ fetchedAt: Date.now(), packument }));
62
+ renameSync(tmp, path);
63
+ return packument;
64
+ }
65
+ }
66
+ export function trim(raw) {
67
+ const versions = {};
68
+ for (const [version, meta] of Object.entries(raw.versions ?? {})) {
69
+ const kept = { version };
70
+ if (meta.peerDependencies)
71
+ kept.peerDependencies = meta.peerDependencies;
72
+ if (meta.peerDependenciesMeta)
73
+ kept.peerDependenciesMeta = meta.peerDependenciesMeta;
74
+ if (meta.dependencies)
75
+ kept.dependencies = meta.dependencies;
76
+ if (meta.engines && typeof meta.engines === 'object')
77
+ kept.engines = meta.engines;
78
+ versions[version] = kept;
79
+ }
80
+ const time = {};
81
+ for (const version of Object.keys(versions))
82
+ if (raw.time?.[version])
83
+ time[version] = raw.time[version];
84
+ return { name: raw.name, 'dist-tags': raw['dist-tags'] ?? {}, versions, time };
85
+ }