Skip to content

Commit 20fb6a0

Browse files
ljharbowlstronaut
authored andcommitted
feat(arborist): add lockfileString() for in-memory lockfile generation
Exposes a public method on the Arborist class that builds (or reuses) an ideal tree, commits the shrinkwrap metadata, and returns the lockfile contents as a string without writing to disk. This makes a previously-undocumented sequence (`buildIdealTree()` -> `tree.meta.commit()` -> `String(tree.meta)`) a discoverable, supported API, enabling callers to inspect, diff, or store generated lockfiles without mutating the project's package-lock.json. (cherry picked from commit b8655c7)
1 parent 3298369 commit 20fb6a0

2 files changed

Lines changed: 81 additions & 0 deletions

File tree

workspaces/arborist/lib/arborist/index.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,16 @@ class Arborist extends Base {
288288
return ret
289289
}
290290

291+
// Build an ideal tree (or reuse an already-built one) and return the
292+
// resulting lockfile contents as a string, without writing to disk.
293+
// Useful for callers that want to inspect, diff, or store a lockfile
294+
// somewhere other than the project's `package-lock.json`.
295+
async lockfileString (options = {}) {
296+
await this.buildIdealTree(options)
297+
298+
return this.idealTree.meta.toString(options)
299+
}
300+
291301
async dedupe (options = {}) {
292302
// allow the user to set options on the ctor as well.
293303
// XXX: deprecate separate method options objects.

workspaces/arborist/test/arborist/index.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,74 @@ t.test('valid global/installStrategy values', t => {
253253
t.equal(new Arborist({ installStrategy: 'hoisted' }).options.installStrategy, 'hoisted')
254254
t.end()
255255
})
256+
257+
t.test('lockfileString', async t => {
258+
const fs = require('node:fs')
259+
const { resolve } = require('node:path')
260+
261+
t.test('returns the lockfile contents as a string without writing to disk', async t => {
262+
const path = t.testdir({
263+
'package.json': JSON.stringify({
264+
name: 'lockfile-string-test',
265+
version: '1.0.0',
266+
}),
267+
})
268+
269+
const arb = new Arborist({ path })
270+
const str = await arb.lockfileString()
271+
272+
t.type(str, 'string', 'returns a string')
273+
const parsed = JSON.parse(str)
274+
t.equal(parsed.name, 'lockfile-string-test')
275+
t.ok(parsed.lockfileVersion, 'has a lockfileVersion')
276+
t.ok(parsed.packages, 'has a packages map')
277+
t.equal(
278+
fs.existsSync(resolve(path, 'package-lock.json')),
279+
false,
280+
'no package-lock.json was written to disk'
281+
)
282+
})
283+
284+
t.test('reuses an already-built ideal tree', async t => {
285+
const path = t.testdir({
286+
'package.json': JSON.stringify({
287+
name: 'lockfile-string-reuse',
288+
version: '1.0.0',
289+
}),
290+
})
291+
292+
const arb = new Arborist({ path })
293+
const tree = await arb.buildIdealTree()
294+
const str = await arb.lockfileString()
295+
296+
t.equal(arb.idealTree, tree, 'did not rebuild the tree')
297+
t.equal(JSON.parse(str).name, 'lockfile-string-reuse')
298+
})
299+
300+
t.test('respects lockfileVersion option', async t => {
301+
const path = t.testdir({
302+
'package.json': JSON.stringify({
303+
name: 'lockfile-string-version',
304+
version: '1.0.0',
305+
}),
306+
})
307+
308+
const arb = new Arborist({ path, lockfileVersion: 2 })
309+
const str = await arb.lockfileString()
310+
311+
t.equal(JSON.parse(str).lockfileVersion, 2)
312+
})
313+
314+
t.test('does not modify an existing lockfile on disk', async t => {
315+
const fixture = resolve(__dirname, '../fixtures/workspaces-simple-virtual')
316+
const before = fs.readFileSync(resolve(fixture, 'package-lock.json'), 'utf8')
317+
318+
const arb = new Arborist({ path: fixture })
319+
const str = await arb.lockfileString()
320+
321+
const after = fs.readFileSync(resolve(fixture, 'package-lock.json'), 'utf8')
322+
t.equal(after, before, 'fixture lockfile is unchanged')
323+
t.type(str, 'string')
324+
t.equal(JSON.parse(str).name, 'workspace-simple')
325+
})
326+
})

0 commit comments

Comments
 (0)