@exodus/stasis 1.0.0-beta.7 → 1.0.0-beta.9

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.
@@ -1,7 +1,8 @@
1
1
  import { constants } from 'node:fs'
2
- import { basename, dirname, join, relative, resolve, sep } from 'node:path'
2
+ import { basename, dirname, join, posix, relative, resolve, sep } from 'node:path'
3
3
 
4
- import { NO_ENTRY, packageJSONStat, packageJSONText, readJson } from '@exodus/stasis-core/bundle-util'
4
+ import { isValidRepoField } from '@exodus/stasis-core/bundle'
5
+ import { NO_ENTRY, packageJSONStat, packageJSONText, parseGithubRepository, readJson } from '@exodus/stasis-core/bundle-util'
5
6
  import { byName } from '@exodus/stasis-core/host'
6
7
  import { hasNodeModulesSegment, isPlainObject } from '@exodus/stasis-core/util'
7
8
  import { buildNpmTree, findNpmWorkspaces } from '@preventive/deptree/npm.js'
@@ -114,6 +115,14 @@ export function checkTarget(name, { os, cpu, libc }) {
114
115
  if (libc !== undefined && !LIBCS.has(libc)) throw new TypeError(`${name}: libc must be one of 'glibc', 'musl', 'unknown'`)
115
116
  }
116
117
 
118
+ // `cache`, where npm packages' tarballs and version documents are kept, as @preventive/upstream's
119
+ // CacheOptions take it: left out, in setCacheDir's cache; a store with `read` and `write`, in place
120
+ // of it; or false, nowhere. Checked as deptree checks it, before anything is fetched.
121
+ export function checkCache(name, cache) {
122
+ if (cache === undefined || cache === false || (typeof cache?.read === 'function' && typeof cache?.write === 'function')) return
123
+ throw new TypeError(`${name}: cache must be false, or a store with read and write, or left out`)
124
+ }
125
+
117
126
  // yarn installs a workspace from the root that declares it, whatever yarn.lock is nearer, and any
118
127
  // other package from the nearest yarn.lock.
119
128
  function yarn1Root(host, cwd) {
@@ -191,7 +200,7 @@ const PACKAGE_MANAGERS = {
191
200
  alone: async (names, above) => !(await above()).some((dir) => dir.includes('pnpm-workspace.yaml')),
192
201
  projects: (view, pnpm) => findPnpmProjects({ project: view, host: { pnpm } }),
193
202
  installs: 'node_modules',
194
- build: (view, pnpm, _file, given) => buildPnpmTree({ project: view, host: { pnpm, ...target(given) } }),
203
+ build: (view, pnpm, _file, given) => buildPnpmTree({ project: view, host: { pnpm, ...target(given) }, cache: given?.cache }),
195
204
  },
196
205
  yarn1: {
197
206
  kind: 'js',
@@ -205,7 +214,7 @@ const PACKAGE_MANAGERS = {
205
214
  installs: 'node_modules',
206
215
  build: (view, yarn, file, given) => {
207
216
  const { libc: _, ...host } = target({ ...given, libc: 'unknown' }) // yarn 1 matches no libc
208
- return naming(file, buildYarn1Tree({ project: view, host: { yarn, ...host } }))
217
+ return naming(file, buildYarn1Tree({ project: view, host: { yarn, ...host }, cache: given?.cache }))
209
218
  },
210
219
  },
211
220
  npm: {
@@ -218,7 +227,7 @@ const PACKAGE_MANAGERS = {
218
227
  alone: async (names, above) => !(await above()).some((dir) => dir.includes('package.json')),
219
228
  projects: (view, _npm, given) => findNpmWorkspaces({ project: view, os: target(given).os }),
220
229
  installs: 'node_modules',
221
- build: (view, npm, file, given) => naming(file, buildNpmTree({ project: view, host: npmHost(npm, given) })),
230
+ build: (view, npm, file, given) => naming(file, buildNpmTree({ project: view, host: npmHost(npm, given), cache: given?.cache })),
222
231
  },
223
232
  soldeer: {
224
233
  kind: 'sol',
@@ -343,15 +352,17 @@ export function checkVfs(name, vfs) {
343
352
  }
344
353
  }
345
354
 
346
- // -> { root, vfs, projects, stats, packageManager, packageManagerVersion }, of the project `project`
347
- // holds that `cwd` is in, as `packageManager` installs it: the directory it installs from, as a real
348
- // path; a new Vfs holding the tree, rooted there; the directories of the projects it finds, from
349
- // there; deptree's counts; and the version reproduced, `packageManagerVersion` if given, else the one
350
- // the root package.json's packageManager pins, else the default. deptree reads the project through a
351
- // view of `project`, which nothing is written through. `client`, a @preventive/upstream/github.js
352
- // client, is what a Soldeer git dependency is fetched through (deptree's `github`); without one, a
353
- // lockfile holding one is refused before anything is fetched.
354
- async function layOutTree({ project, packageManager, cwd, packageManagerVersion, os, cpu, libc, client }) {
355
+ // -> { root, vfs, projects, stats, installed, packageManager, packageManagerVersion }, of the project
356
+ // `project` holds that `cwd` is in, as `packageManager` installs it: the directory it installs from,
357
+ // as a real path; a new Vfs holding the tree, rooted there; the directories of the projects it finds,
358
+ // from there; deptree's counts, and what it installs, each by its `path` from there; and the version
359
+ // reproduced, `packageManagerVersion` if given, else the one the root package.json's packageManager
360
+ // pins, else the default. deptree reads the project through a view of `project`, which nothing is
361
+ // written through. `client`, a @preventive/upstream/github.js client, is what a Soldeer git
362
+ // dependency is fetched through (deptree's `github`); without one, a lockfile holding one is refused
363
+ // before anything is fetched. `cache` (checkCache) is where npm packages' tarballs and version
364
+ // documents are kept, which deptree fetches for pnpm, yarn1 and npm.
365
+ async function layOutTree({ project, packageManager, cwd, packageManagerVersion, os, cpu, libc, client, cache }) {
355
366
  const pm = PACKAGE_MANAGERS[packageManager]
356
367
  const found = pm.root(project, cwd, os)
357
368
  if (found === null) throw new Error(`no ${pm.lockfile} found in ${cwd} or any parent directory`)
@@ -364,17 +375,40 @@ async function layOutTree({ project, packageManager, cwd, packageManagerVersion,
364
375
  const projects = new Set(pm.projects(view, version, { os, cpu, libc }))
365
376
  const other = pm.kind === 'js' ? outsider(project, found, cwd, projects) : null
366
377
  if (other !== null) throw new Error(`${file} does not install ${other}: it is none of the lockfile's projects`)
367
- const { vfs, stats } = await pm.build(view, version, file, { os, cpu, libc, client })
378
+ const { vfs, stats, installed } = await pm.build(view, version, file, { os, cpu, libc, client, cache })
368
379
  return {
369
380
  root: project.realpath(found),
370
381
  vfs,
371
382
  projects,
372
383
  stats,
384
+ installed,
373
385
  packageManager,
374
386
  packageManagerVersion: version ?? (String(pinned).startsWith(`${pm.pin}@`) ? /^[^@]+@([^+]+)/u.exec(pinned)[1] : undefined),
375
387
  }
376
388
  }
377
389
 
390
+ // The root of the GitHub repository a git URL names, or undefined for another host's.
391
+ const githubRoot = (url) => {
392
+ const github = parseGithubRepository(url)
393
+ return github === null ? undefined : { github, directory: '' }
394
+ }
395
+
396
+ // Each dependency of `bundle`, its paths from `root`, at the commit the tree (layOutTree's) installs
397
+ // it from, where deptree names a full one: an npm package at its gitHead, which the registry's
398
+ // version document names, a commit of the repository its own package.json names, so taken only
399
+ // where the bundle records that one; a Soldeer git dependency at its lockfile's `rev`, the root of
400
+ // the repository it is fetched from, where that is a GitHub one. Metadata, as `repo` is: the
401
+ // publisher's word, held to no repository.
402
+ export function pinCommits(bundle, root, { root: from, installed }) {
403
+ const byDir = new Map(installed.map((pkg) => [posix.relative(root, posix.join(from, pkg.path)), pkg]))
404
+ for (const [dir, info] of bundle.modules) {
405
+ const pkg = byDir.get(dir)
406
+ if (!isValidRepoField('commit', pkg?.commit)) continue
407
+ const repo = info.repo ?? githubRoot(pkg.git)
408
+ if (repo !== undefined) bundle.modules.set(dir, { ...info, repo: { ...repo, commit: pkg.commit } })
409
+ }
410
+ }
411
+
378
412
  // fs.Stats#mode carries the file type above the permission bits a Vfs stat holds.
379
413
  const TYPE_BITS = { file: constants.S_IFREG, directory: constants.S_IFDIR, symlink: constants.S_IFLNK }
380
414
 
package/src/vfs-bundle.js CHANGED
@@ -3,7 +3,7 @@ import { posix } from 'node:path'
3
3
  import { Vfs } from '@preventive/vfs'
4
4
  import { KINDS, checkAhead } from './vfs-bundle/entries.js'
5
5
  import { suggestedRepoEntries } from './vfs-bundle/github.js'
6
- import { NODE_MODULES_MANAGERS, checkTarget, checkVfs, detectPackageManager, loadTree, packageManagerFor, vfsHost } from './vfs-bundle/tree.js'
6
+ import { NODE_MODULES_MANAGERS, checkCache, checkTarget, checkVfs, detectPackageManager, loadTree, packageManagerFor, vfsHost } from './vfs-bundle/tree.js'
7
7
 
8
8
  // @exodus/stasis/vfs-bundle: static bundles from a project's lockfile alone, through the
9
9
  // dependencies its package manager would install (`packageManager`: 'pnpm', pnpm 9, 10, 11 or 12;
@@ -30,10 +30,12 @@ export function createVfsHost(vfs) {
30
30
  return vfsHost(vfs, { cache: false })
31
31
  }
32
32
 
33
- // -> { root, vfs, projects, host, stats, packageManager, packageManagerVersion }, as 'pnpm',
34
- // 'yarn1' or 'npm' installs node_modules: the directory in the project's Vfs it installs cwd from,
35
- // which holds the lockfile; the Vfs the tree is laid out into, rooted there; the projects'
36
- // directories from there; the host reading the project through the tree; deptree's counts; and the
33
+ // -> { root, vfs, projects, host, stats, installed, packageManager, packageManagerVersion }, as
34
+ // 'pnpm', 'yarn1' or 'npm' installs node_modules: the directory in the project's Vfs it installs cwd
35
+ // from, which holds the lockfile; the Vfs the tree is laid out into, rooted there; the projects'
36
+ // directories from there; the host reading the project through the tree; deptree's counts, and the
37
+ // packages it installs (each by its `path` from there, with the `commit` its version document
38
+ // names, where it names a full one); and the
37
39
  // package manager reproduced, at `packageManagerVersion` if given, else the one the root
38
40
  // package.json's packageManager pins, else pnpm 10.33.4, yarn 1.22.22 or npm 11.21.0 (which nothing
39
41
  // pins: npm reads no packageManager). The host caches what it reads, of the project's Vfs too, so
@@ -41,15 +43,18 @@ export function createVfsHost(vfs) {
41
43
  // `os`, `cpu` and `libc` ('glibc', 'musl' or 'unknown', pnpm's and npm's, which takes 'unknown' for
42
44
  // none) are the machine packages are matched against: this one's but for what is given (for another
43
45
  // os, libc defaults to 'unknown'). Without a `packageManager`, it is the one of the three whose
44
- // lockfile installs cwd, where only one's does.
45
- export async function loadNodeModules({ vfs, packageManager, cwd = '/', packageManagerVersion, os, cpu, libc } = {}) {
46
+ // lockfile installs cwd, where only one's does. `cache` is where the packages' tarballs and version
47
+ // documents are kept, as @preventive/upstream's CacheOptions take it: left out, in setCacheDir's
48
+ // cache; a store with `read` and `write`, in place of it; or false, nowhere.
49
+ export async function loadNodeModules({ vfs, packageManager, cwd = '/', packageManagerVersion, os, cpu, libc, cache } = {}) {
46
50
  checkVfs('loadNodeModules', vfs)
47
51
  checkTarget('loadNodeModules', { os, cpu, libc })
52
+ checkCache('loadNodeModules', cache)
48
53
  const project = vfsHost(vfs)
49
54
  // A real path, as detection and the layout take it.
50
55
  cwd = project.realpath(posix.resolve('/', cwd))
51
56
  packageManager = packageManagerFor('loadNodeModules', project, cwd, { packageManager, packageManagerVersion, os }, NODE_MODULES_MANAGERS)
52
- return loadTree({ project, packageManager, cwd, packageManagerVersion, os, cpu, libc })
57
+ return loadTree({ project, packageManager, cwd, packageManagerVersion, os, cpu, libc, cache })
53
58
  }
54
59
 
55
60
  // -> the entries buildGitHubBundle takes where none are given, as paths from the project's