profile-plugins.spec.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. /** Installed metadata and shared bundle activation across profile package operations. */
  2. import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
  3. import { tmpdir } from 'node:os'
  4. import { dirname, join } from 'node:path'
  5. import { afterEach, describe, expect, it } from 'vitest'
  6. import { readProfilePlugins, reconcileProfilePlugins, writeProfileBundles, type ProfilePluginLocation } from '../src/profile-plugins.ts'
  7. const roots: string[] = []
  8. afterEach(() => {
  9. for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
  10. })
  11. function writeManifest(dir: string, value: unknown): void {
  12. mkdirSync(dir, { recursive: true })
  13. writeFileSync(join(dir, 'package.json'), JSON.stringify(value))
  14. }
  15. function fixture(): ProfilePluginLocation {
  16. const root = mkdtempSync(join(tmpdir(), 'dsh-profile-plugins-'))
  17. roots.push(root)
  18. const profileDir = join(root, 'profile')
  19. const installAnchor = join(root, 'runtime', 'package.json')
  20. writeManifest(dirname(installAnchor), { name: 'fixture-runtime' })
  21. writeManifest(profileDir, {})
  22. return { binName: 'test', profileDir, installAnchor }
  23. }
  24. function installed(location: ProfilePluginLocation, name: string, bundle = true): void {
  25. writeManifest(join(location.profileDir, 'node_modules', name), {
  26. name, version: '1.0.0', ...(bundle ? { dsh: { bundle: { patch: 'unbuilt.yml' } } } : {}),
  27. })
  28. }
  29. describe('profile plugin inventory', () => {
  30. it('uses dependency alias keys, installed versions, and the loader installation precedence', () => {
  31. const location = fixture()
  32. writeManifest(location.profileDir, {
  33. dependencies: { alias: 'file:../local-package', shared: '^2', library: '^1', missing: 'git+example', unversioned: 'file:../unversioned' },
  34. dsh: { profile: { bundles: ['shared', 'alias'] } },
  35. })
  36. const source = join(location.profileDir, '..', 'local-package')
  37. writeManifest(source, { name: 'original-name', version: '3.0.0', dsh: { bundle: { patch: 'missing.yml' } } })
  38. mkdirSync(join(location.profileDir, 'node_modules'))
  39. symlinkSync(source, join(location.profileDir, 'node_modules', 'alias'), 'junction')
  40. installed(location, 'shared', false)
  41. installed(location, 'library', false)
  42. writeManifest(join(location.profileDir, 'node_modules', 'unversioned'), { name: 'unversioned' })
  43. writeManifest(join(dirname(location.installAnchor), 'node_modules', 'shared'), {
  44. name: 'shared', version: '9.0.0', dsh: { bundle: { patch: 'bundle.yml' } },
  45. })
  46. expect(readProfilePlugins(location).dependencies).toEqual([
  47. { name: 'alias', version: '3.0.0', bundle: true, enabled: true },
  48. { name: 'shared', version: '1.0.0', bundle: true, enabled: true },
  49. { name: 'library', version: '1.0.0', bundle: false, enabled: false },
  50. { name: 'missing', version: 'git+example', bundle: false, enabled: false },
  51. { name: 'unversioned', version: 'file:../unversioned', bundle: false, enabled: false },
  52. ])
  53. })
  54. it('keeps malformed installed metadata visible and accepts an empty profile', () => {
  55. const location = fixture()
  56. expect(readProfilePlugins(location)).toEqual({ manifest: {}, dependencies: [] })
  57. writeManifest(location.profileDir, { dependencies: { broken: '^1', invalid: '^2' } })
  58. installed(location, 'broken')
  59. writeFileSync(join(location.profileDir, 'node_modules', 'broken', 'package.json'), '{')
  60. writeManifest(join(location.profileDir, 'node_modules', 'invalid'), [])
  61. expect(readProfilePlugins(location).dependencies).toEqual([
  62. { name: 'broken', version: '^1', bundle: false, enabled: false },
  63. { name: 'invalid', version: '^2', bundle: false, enabled: false },
  64. ])
  65. })
  66. })
  67. describe('profile plugin reconciliation', () => {
  68. it.each([false, true])('reconciles declarations and removal while preserveDisabled=%s', (preserveDisabled) => {
  69. const location = fixture()
  70. const beforeManifest = {
  71. name: 'custom-profile', private: false, custom: { keep: true },
  72. dependencies: { active: '^1', disabled: '^1', gaining: '^1', losing: '^1', removed: '^1' },
  73. dsh: { profile: { bundles: ['template', 'template', 'active', 'losing', 'removed'] } },
  74. }
  75. writeManifest(location.profileDir, beforeManifest)
  76. for (const name of Object.keys(beforeManifest.dependencies)) installed(location, name, name !== 'gaining')
  77. const before = readProfilePlugins(location)
  78. writeManifest(location.profileDir, {
  79. ...beforeManifest,
  80. dependencies: { active: '^2', disabled: '^2', gaining: '^2', losing: '^2', added: '^1', 'new-library': '^1' },
  81. })
  82. installed(location, 'gaining')
  83. installed(location, 'losing', false)
  84. installed(location, 'added')
  85. installed(location, 'new-library', false)
  86. const result = reconcileProfilePlugins({ ...location, before, preserveDisabled })
  87. const bundles = ['template', 'template', 'active', ...preserveDisabled ? [] : ['disabled'], 'gaining', 'added']
  88. expect(result.plugins.manifest.dsh?.profile?.bundles).toEqual(bundles)
  89. expect(result.plugins.dependencies.filter(dependency => dependency.enabled).map(dependency => dependency.name))
  90. .toEqual(['active', ...preserveDisabled ? [] : ['disabled'], 'gaining', 'added'])
  91. expect(result.addedPlainDependencies).toEqual(['new-library'])
  92. expect(JSON.parse(readFileSync(join(location.profileDir, 'package.json'), 'utf8'))).toMatchObject({
  93. custom: { keep: true }, private: false, dsh: { profile: { bundles } },
  94. })
  95. const bytes = readFileSync(join(location.profileDir, 'package.json'), 'utf8')
  96. expect(reconcileProfilePlugins({ ...location, before: result.plugins, preserveDisabled }).addedPlainDependencies).toEqual([])
  97. expect(readFileSync(join(location.profileDir, 'package.json'), 'utf8')).toBe(bytes)
  98. })
  99. it('writes newly activated bundles when profile metadata is absent', () => {
  100. const location = fixture()
  101. const before = readProfilePlugins(location)
  102. writeManifest(location.profileDir, { dependencies: { added: '^1' } })
  103. installed(location, 'added')
  104. expect(reconcileProfilePlugins({ ...location, before, preserveDisabled: false }).plugins.manifest.dsh?.profile?.bundles)
  105. .toEqual(['added'])
  106. })
  107. it('removes an unresolved dependency layer while preserving custom profile metadata', () => {
  108. const location = fixture()
  109. writeManifest(location.profileDir, { dependencies: { missing: '^1' }, dsh: { profile: { bundles: ['missing', 'template'] } } })
  110. const before = readProfilePlugins(location)
  111. const reconciled = reconcileProfilePlugins({ ...location, before, preserveDisabled: true })
  112. expect(reconciled.plugins.manifest.dsh?.profile?.bundles).toEqual(['template'])
  113. expect(writeProfileBundles(location.profileDir, reconciled.plugins.manifest, ['template', 'manual']).dsh?.profile?.bundles)
  114. .toEqual(['template', 'manual'])
  115. })
  116. })