ensureExternals
uses node:fs
Install what exts import: each package at its locked version (picking one for ranges not locked yet), under
its range's alias, with DefinitelyTyped types (@types/react) beside packages that ship none.
import { compareVersions, compatRange, satisfies as satisfies_ } from "#codebase/tcmts/core/semver"; import { HOST_MODULES } from "#codebase/tcmts/core/types"; import type { ExtRef } from "#codebase/tcmts/core/types"; import type { Layout } from "#codebase/tcmts/loader"; import { candidates, checkIntegrity, hasTypes, install, installedVersion, linkPeers, lockOf, newestVersion, npmAlias, packageManager, parseModule, pickVersions, shippedPackage, shippedPins, viewed, writeIfChanged, } from "#codebase/tcmts/loader/packages"; import type { CachePackageJson } from "#codebase/tcmts/loader/packages"; import fs from "node:fs"; import path from "node:path"; export function ensureExternals(lay: Layout, exts: Iterable<ExtRef>): void { const lock = lockOf(lay); const wanted = new Map<string, { name: string; range: string }>(); for (const e of exts) { // a module where the code runs provides (`vscode`): only its types, installed under their own name, where // TypeScript looks for them; at the newest range the codebase has locked them at, else the newest release const types = HOST_MODULES.get(e.module); if (types) { if (wanted.has(types)) continue; const locked = lock .all() .filter((l) => l.name === types) .sort((x, y) => compareVersions(y.version, x.version))[0]; const latest = locked ? undefined : newestVersion(types); const range = locked?.range ?? (latest && compatRange(latest)); if (range) wanted.set(types, { name: types, range }); continue; } const module = e.module; if (!module.startsWith("npm:")) continue; const { name, version } = parseModule(module); const range = compatRange(version); wanted.set(npmAlias(module), { name, range }); } if (!wanted.size) return; // a package tcm ships (Vitest, which tests import), at a version in the range, unless the codebase chose another: tcm's // own, linked into the cache, not installed (one copy: the one tcm runs tests with is the one they import) const pins = shippedPins.get(lay.cacheDir); const linked = new Map<string, string>(); // (nor one installed in this cache: a tcm run from its codebase ships what it installed there, which linked to // itself would leave the install, and be pruned) const cache = fs.existsSync(lay.cacheDir) ? fs.realpathSync(lay.cacheDir) + path.sep : undefined; for (const [alias, w] of wanted) { const own = shippedPackage(w.name); const pin = pins?.(w.name); if ( !own || (cache && own.dir.startsWith(cache)) || !satisfies_(own.version, w.range) || (pin && !satisfies_(own.version, pin)) ) continue; if (lock.get(w.name, w.range) !== own.version) lock.set(w.name, w.range, own.version); linked.set(alias, own.dir); } const unlocked = [...wanted.values()].filter((w) => !lock.get(w.name, w.range)); if (unlocked.length) pickVersions(unlocked, lock); const file = path.join(lay.cacheDir, "package.json"); const pkg = ( fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, "utf8")) : {} ) as CachePackageJson; const deps = { ...pkg.dependencies }; const untyped = new Set(pkg.tcmUntyped ?? []); const write = () => writeIfChanged( file, JSON.stringify( { name: "tcm-cache", private: true, type: "module", dependencies: deps, ...(untyped.size ? { tcmUntyped: [...untyped].sort() } : {}), }, null, 2, ) + "\n", ); const stale = (alias: string, version: string) => installedVersion(lay, alias) !== version; const missing: string[] = []; for (const [alias, w] of wanted) { const own = linked.get(alias); if (own) { // (npm has no `link:`: its `file:` of a directory is a link too) deps[alias] = `${packageManager() === "pnpm" ? "link" : "file"}:${own}`; const at = path.join(lay.cacheDir, "node_modules", alias); let there: string | undefined; try { there = fs.realpathSync(at); } catch { there = undefined; } if (there !== fs.realpathSync(own)) { fs.rmSync(at, { recursive: true, force: true }); fs.mkdirSync(path.dirname(at), { recursive: true }); fs.symlinkSync(own, at, "dir"); } continue; } const version = lock.get(w.name, w.range)!; deps[alias] = `npm:${w.name}@${version}`; if (stale(alias, version)) missing.push(`${w.name}@${version}`); } // a package being installed whose release names no types of its own (as the registry said when its version was // picked): its DefinitelyTyped ones come in the same install, and what it turns out to have decides, below const guessed = new Set<string>(); const noTypes = new Set<string>(); for (const [alias, w] of wanted) { const version = lock.get(w.name, w.range)!; if ( linked.has(alias) || deps[`@types/${alias}`] || untyped.has(alias) || !stale(alias, version) ) continue; if (viewed.get(`${w.name}@${w.range}`)?.find((c) => c.version === version)?.typed !== false) continue; const typesName = `@types/${w.name.replace(/^@/, "").replace("/", "__")}`; const typesVersion = lock.get(typesName, w.range) ?? candidates(typesName, w.range)[0]?.version; if (!typesVersion) { noTypes.add(alias); continue; } // (locked once it's found to be needed, below) deps[`@types/${alias}`] = `npm:${typesName}@${typesVersion}`; if (stale(`@types/${alias}`, typesVersion)) missing.push(`${typesName}@${typesVersion}`); guessed.add(alias); } write(); if (missing.length) install(lay, missing); // types for packages without their own, under the alias's name: where TypeScript looks for the alias's types const typeMissing: string[] = []; for (const [alias, w] of wanted) { const typesAlias = `@types/${alias}`; // (what tcm ships has its types) if (linked.has(alias)) continue; // (one with types of its own after all: the ones installed with it aren't its) if (guessed.has(alias) && hasTypes(lay, alias)) { delete deps[typesAlias]; continue; } if (noTypes.has(alias) && !hasTypes(lay, alias)) { untyped.add(alias); continue; } if (guessed.has(alias)) { const typesName = `@types/${w.name.replace(/^@/, "").replace("/", "__")}`; if (!lock.get(typesName, w.range)) lock.set( typesName, w.range, deps[typesAlias]!.slice(deps[typesAlias]!.lastIndexOf("@") + 1), ); continue; } if (deps[typesAlias] || untyped.has(alias) || hasTypes(lay, alias)) { if ( deps[typesAlias] && stale(typesAlias, deps[typesAlias]!.slice(deps[typesAlias]!.lastIndexOf("@") + 1)) ) typeMissing.push(deps[typesAlias]!.slice(4)); continue; } const typesName = `@types/${w.name.replace(/^@/, "").replace("/", "__")}`; let version = lock.get(typesName, w.range); if (!version) { version = candidates(typesName, w.range)[0]?.version; if (version) lock.set(typesName, w.range, version); } if (!version) { untyped.add(alias); continue; } deps[typesAlias] = `npm:${typesName}@${version}`; if (stale(typesAlias, version)) typeMissing.push(`${typesName}@${version}`); } write(); if (typeMissing.length) install(lay, typeMissing); if (missing.length || typeMissing.length) { checkIntegrity(lay, lock); linkPeers(lay, lock); } }
uses
- tcmts.core.semver.compareVersions
- tcmts.core.semver.compatRange
- tcmts.core.semver.satisfies
- tcmts.core.types.ExtRef
- tcmts.core.types.HOST_MODULES
- tcmts.loader.Layout
- tcmts.loader.packages.CachePackageJson
- tcmts.loader.packages.candidates
- tcmts.loader.packages.checkIntegrity
- tcmts.loader.packages.hasTypes
- tcmts.loader.packages.install
- tcmts.loader.packages.installedVersion
- tcmts.loader.packages.linkPeers
- tcmts.loader.packages.lockOf
- tcmts.loader.packages.newestVersion
- tcmts.loader.packages.npmAlias
- tcmts.loader.packages.packageManager
- tcmts.loader.packages.parseModule
- tcmts.loader.packages.pickVersions
- tcmts.loader.packages.shippedPackage
- tcmts.loader.packages.shippedPins
- tcmts.loader.packages.viewed
- tcmts.loader.packages.writeIfChanged
used by
- tcmts.cli.codebase.Codebase
- tcmts.cli.flight.flightPackage
- tcmts.cli.libraries.libraryEffects
- tcmts.cli.lint.provideTool
- tcmts.cli.package_pages.packagePage
- tcmts.cli.provided.providePackages
- tcmts.cli.upgrade.upgradeTo
- tcmts.cli.upgrade.upgradeWithin
- tcmts.cli.vitest.prepareTestRun
- tcmts.cli.web.planApp
- tcmts.loader.materialize