1
0

profile-plugins.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. /** Installed profile dependencies and their bundle activation after package-manager operations. */
  2. import { join } from 'node:path'
  3. import { readProfileManifest, resolveBundleDir, writeProfileManifest, type ProfileManifest } from './profile.ts'
  4. /** Profile directory and installation used by the shared bundle resolver. */
  5. export interface ProfilePluginLocation {
  6. /** Diagnostic prefix for profile manifest failures. */
  7. readonly binName: string
  8. /** Profile package directory managed by pnpm. */
  9. readonly profileDir: string
  10. /** Absolute package.json path of the owning dsh installation. */
  11. readonly installAnchor: string
  12. }
  13. /** One dependency under its package-manager key, including npm aliases. */
  14. export interface ProfilePluginDependency {
  15. /** Dependency key used in package.json and the bundle list. */
  16. readonly name: string
  17. /** Installed version, or the dependency spec when installed metadata is unavailable. */
  18. readonly version: string
  19. /** Whether the package selected by the bundle resolver declares a patch. */
  20. readonly bundle: boolean
  21. /** Whether the dependency appears in the profile's active bundle list. */
  22. readonly enabled: boolean
  23. }
  24. /** Profile manifest and dependencies in manifest order. */
  25. export interface ProfilePluginInventory {
  26. readonly manifest: ProfileManifest
  27. readonly dependencies: readonly ProfilePluginDependency[]
  28. }
  29. /** Result of reconciling bundle activation after a successful package operation. */
  30. export interface ProfilePluginReconciliation {
  31. readonly plugins: ProfilePluginInventory
  32. /** Newly added ordinary dependencies, for optional caller-owned diagnostics. */
  33. readonly addedPlainDependencies: readonly string[]
  34. }
  35. /** Unavailable installed metadata must not prevent listing or removing dependencies. */
  36. function optionalManifest(binName: string, packageDir: string): ProfileManifest | undefined {
  37. try { return readProfileManifest(binName, packageDir) } catch { return undefined }
  38. }
  39. /** Resolve activation metadata with the same installation precedence as profile loading. */
  40. function bundleManifest(location: ProfilePluginLocation, name: string): ProfileManifest | undefined {
  41. let packageDir: string
  42. try {
  43. packageDir = resolveBundleDir(location.binName, name, location.installAnchor, location.profileDir)
  44. } catch {
  45. // An unresolved dependency remains visible as an ordinary package.
  46. return undefined
  47. }
  48. return optionalManifest(location.binName, packageDir)
  49. }
  50. /**
  51. * Read installed versions and bundle declarations without requiring loadable plugin code.
  52. * Missing or unreadable installed metadata leaves the dependency visible for repair or removal.
  53. * @param location - profile and installation resolution inputs.
  54. * @returns dependency records in package.json order and the profile manifest.
  55. */
  56. export function readProfilePlugins(location: ProfilePluginLocation): ProfilePluginInventory {
  57. const manifest = readProfileManifest(location.binName, location.profileDir)
  58. const bundles = manifest.dsh?.profile?.bundles ?? []
  59. const dependencies = Object.entries(manifest.dependencies ?? {}).map(([name, spec]) => {
  60. const installed = optionalManifest(location.binName, join(location.profileDir, 'node_modules', name))
  61. return {
  62. name,
  63. version: typeof installed?.version === 'string' ? installed.version : spec,
  64. bundle: bundleManifest(location, name)?.dsh?.bundle?.patch !== undefined,
  65. enabled: bundles.includes(name),
  66. }
  67. })
  68. return { manifest, dependencies }
  69. }
  70. /**
  71. * Write a bundle list while preserving the supplied profile's other metadata.
  72. * @param profileDir - directory whose package.json is updated.
  73. * @param manifest - current profile manifest, read after the package operation when applicable.
  74. * @param bundles - ordered active bundle names, including any template entries.
  75. * @returns the written manifest.
  76. */
  77. export function writeProfileBundles(
  78. profileDir: string, manifest: ProfileManifest, bundles: readonly string[],
  79. ): ProfileManifest {
  80. const updated = { ...manifest, dsh: { ...manifest.dsh, profile: { ...manifest.dsh?.profile, bundles: [...bundles] } } }
  81. writeProfileManifest(profileDir, updated)
  82. return updated
  83. }
  84. /**
  85. * Reconcile installed bundle declarations after a successful package-manager operation.
  86. * Dependency-managed entries disappear when removed or when their package loses its declaration;
  87. * template entries retain their order and duplicates. New bundle dependencies activate automatically.
  88. * @param options - location, inventory captured before pnpm, and whether explicitly disabled bundles remain disabled.
  89. * @returns updated inventory and newly added ordinary dependencies for caller-owned warnings.
  90. */
  91. export function reconcileProfilePlugins(options: ProfilePluginLocation & {
  92. readonly before: ProfilePluginInventory
  93. readonly preserveDisabled: boolean
  94. }): ProfilePluginReconciliation {
  95. const after = readProfilePlugins(options)
  96. const beforeNames = new Set(options.before.dependencies.map(dependency => dependency.name))
  97. const afterNames = new Set(after.dependencies.map(dependency => dependency.name))
  98. const bundleNames = new Set(after.dependencies.filter(dependency => dependency.bundle).map(dependency => dependency.name))
  99. const disabled = new Set(options.preserveDisabled
  100. ? options.before.dependencies.filter(dependency => dependency.bundle && !dependency.enabled).map(dependency => dependency.name)
  101. : [])
  102. const previous = after.manifest.dsh?.profile?.bundles ?? []
  103. const bundles = previous.filter(name => !(beforeNames.has(name) || afterNames.has(name)) || bundleNames.has(name))
  104. for (const dependency of after.dependencies) {
  105. if (dependency.bundle && !disabled.has(dependency.name) && !bundles.includes(dependency.name)) bundles.push(dependency.name)
  106. }
  107. const changed = bundles.length !== previous.length || bundles.some((name, index) => name !== previous[index])
  108. const manifest = changed ? writeProfileBundles(options.profileDir, after.manifest, bundles) : after.manifest
  109. return {
  110. plugins: {
  111. manifest,
  112. dependencies: after.dependencies.map(dependency => ({ ...dependency, enabled: bundles.includes(dependency.name) })),
  113. },
  114. addedPlainDependencies: after.dependencies.filter(dependency => !dependency.bundle && !beforeNames.has(dependency.name))
  115. .map(dependency => dependency.name),
  116. }
  117. }